diff --git a/.gitignore b/.gitignore
index cb9f304991..1c2a5e1a0b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
# Python-related files
-__pycache__/
+__pycache__
*.py[cod]
*.egg-info/
.eggs/
@@ -67,9 +67,10 @@ lightrag-dev/
gui/
# unit-test files
-test_*
+# test_*
# Cline files
memory-bank
memory-bank/
.clinerules
+throwaway
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000000..b43bf86b50
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1 @@
+README.md
diff --git a/README.md b/README.md
index 7e9315a38c..7ac4fe6a7d 100644
--- a/README.md
+++ b/README.md
@@ -50,25 +50,27 @@
---
+
## 🎉 News
-- [X] [2025.06.16]🎯📢Our team has released [RAG-Anything](https://github.com/HKUDS/RAG-Anything) an All-in-One Multimodal RAG System for seamless text, image, table, and equation processing.
-- [X] [2025.06.05]🎯📢LightRAG now supports comprehensive multimodal data handling through [RAG-Anything](https://github.com/HKUDS/RAG-Anything) integration, enabling seamless document parsing and RAG capabilities across diverse formats including PDFs, images, Office documents, tables, and formulas. Please refer to the new [multimodal section](https://github.com/HKUDS/LightRAG/?tab=readme-ov-file#multimodal-document-processing-rag-anything-integration) for details.
-- [X] [2025.03.18]🎯📢LightRAG now supports citation functionality, enabling proper source attribution.
-- [X] [2025.02.05]🎯📢Our team has released [VideoRAG](https://github.com/HKUDS/VideoRAG) understanding extremely long-context videos.
-- [X] [2025.01.13]🎯📢Our team has released [MiniRAG](https://github.com/HKUDS/MiniRAG) making RAG simpler with small models.
-- [X] [2025.01.06]🎯📢You can now [use PostgreSQL for Storage](#using-postgresql-for-storage).
-- [X] [2024.12.31]🎯📢LightRAG now supports [deletion by document ID](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete).
-- [X] [2024.11.25]🎯📢LightRAG now supports seamless integration of [custom knowledge graphs](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#insert-custom-kg), empowering users to enhance the system with their own domain expertise.
-- [X] [2024.11.19]🎯📢A comprehensive guide to LightRAG is now available on [LearnOpenCV](https://learnopencv.com/lightrag). Many thanks to the blog author.
-- [X] [2024.11.11]🎯📢LightRAG now supports [deleting entities by their names](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete).
-- [X] [2024.11.09]🎯📢Introducing the [LightRAG Gui](https://lightrag-gui.streamlit.app), which allows you to insert, query, visualize, and download LightRAG knowledge.
-- [X] [2024.11.04]🎯📢You can now [use Neo4J for Storage](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#using-neo4j-for-storage).
-- [X] [2024.10.29]🎯📢LightRAG now supports multiple file types, including PDF, DOC, PPT, and CSV via `textract`.
-- [X] [2024.10.20]🎯📢We've added a new feature to LightRAG: Graph Visualization.
-- [X] [2024.10.18]🎯📢We've added a link to a [LightRAG Introduction Video](https://youtu.be/oageL-1I0GE). Thanks to the author!
-- [X] [2024.10.17]🎯📢We have created a [Discord channel](https://discord.gg/yF2MmDJyGJ)! Welcome to join for sharing and discussions! 🎉🎉
-- [X] [2024.10.16]🎯📢LightRAG now supports [Ollama models](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#quick-start)!
-- [X] [2024.10.15]🎯📢LightRAG now supports [Hugging Face models](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#quick-start)!
+
+- [x] [2025.06.16]🎯📢Our team has released [RAG-Anything](https://github.com/HKUDS/RAG-Anything) an All-in-One Multimodal RAG System for seamless text, image, table, and equation processing.
+- [x] [2025.06.05]🎯📢LightRAG now supports comprehensive multimodal data handling through [RAG-Anything](https://github.com/HKUDS/RAG-Anything) integration, enabling seamless document parsing and RAG capabilities across diverse formats including PDFs, images, Office documents, tables, and formulas. Please refer to the new [multimodal section](https://github.com/HKUDS/LightRAG/?tab=readme-ov-file#multimodal-document-processing-rag-anything-integration) for details.
+- [x] [2025.03.18]🎯📢LightRAG now supports citation functionality, enabling proper source attribution.
+- [x] [2025.02.05]🎯📢Our team has released [VideoRAG](https://github.com/HKUDS/VideoRAG) understanding extremely long-context videos.
+- [x] [2025.01.13]🎯📢Our team has released [MiniRAG](https://github.com/HKUDS/MiniRAG) making RAG simpler with small models.
+- [x] [2025.01.06]🎯📢You can now [use PostgreSQL for Storage](#using-postgresql-for-storage).
+- [x] [2024.12.31]🎯📢LightRAG now supports [deletion by document ID](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete).
+- [x] [2024.11.25]🎯📢LightRAG now supports seamless integration of [custom knowledge graphs](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#insert-custom-kg), empowering users to enhance the system with their own domain expertise.
+- [x] [2024.11.19]🎯📢A comprehensive guide to LightRAG is now available on [LearnOpenCV](https://learnopencv.com/lightrag). Many thanks to the blog author.
+- [x] [2024.11.11]🎯📢LightRAG now supports [deleting entities by their names](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete).
+- [x] [2024.11.09]🎯📢Introducing the [LightRAG Gui](https://lightrag-gui.streamlit.app), which allows you to insert, query, visualize, and download LightRAG knowledge.
+- [x] [2024.11.04]🎯📢You can now [use Neo4J for Storage](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#using-neo4j-for-storage).
+- [x] [2024.10.29]🎯📢LightRAG now supports multiple file types, including PDF, DOC, PPT, and CSV via `textract`.
+- [x] [2024.10.20]🎯📢We've added a new feature to LightRAG: Graph Visualization.
+- [x] [2024.10.18]🎯📢We've added a link to a [LightRAG Introduction Video](https://youtu.be/oageL-1I0GE). Thanks to the author!
+- [x] [2024.10.17]🎯📢We have created a [Discord channel](https://discord.gg/yF2MmDJyGJ)! Welcome to join for sharing and discussions! 🎉🎉
+- [x] [2024.10.16]🎯📢LightRAG now supports [Ollama models](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#quick-start)!
+- [x] [2024.10.15]🎯📢LightRAG now supports [Hugging Face models](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#quick-start)!
@@ -76,9 +78,9 @@

-*Figure 1: LightRAG Indexing Flowchart - Img Caption : [Source](https://learnopencv.com/lightrag/)*
+_Figure 1: LightRAG Indexing Flowchart - Img Caption : [Source](https://learnopencv.com/lightrag/)_

-*Figure 2: LightRAG Retrieval and Querying Flowchart - Img Caption : [Source](https://learnopencv.com/lightrag/)*
+_Figure 2: LightRAG Retrieval and Querying Flowchart - Img Caption : [Source](https://learnopencv.com/lightrag/)_
@@ -88,7 +90,7 @@
The LightRAG Server is designed to provide Web UI and API support. The Web UI facilitates document indexing, knowledge graph exploration, and a simple RAG query interface. LightRAG Server also provide an Ollama compatible interfaces, aiming to emulate LightRAG as an Ollama chat model. This allows AI chat bot, such as Open WebUI, to access LightRAG easily.
-* Install from PyPI
+- Install from PyPI
```bash
pip install "lightrag-hku[api]"
@@ -96,7 +98,7 @@ cp env.example .env
lightrag-server
```
-* Installation from Source
+- Installation from Source
```bash
git clone https://github.com/HKUDS/LightRAG.git
@@ -108,7 +110,7 @@ cp env.example .env
lightrag-server
```
-* Launching the LightRAG Server with Docker Compose
+- Launching the LightRAG Server with Docker Compose
```
git clone https://github.com/HKUDS/LightRAG.git
@@ -118,18 +120,18 @@ cp env.example .env
docker compose up
```
-> Historical versions of LightRAG docker images can be found here: [LightRAG Docker Images]( https://github.com/HKUDS/LightRAG/pkgs/container/lightrag)
+> Historical versions of LightRAG docker images can be found here: [LightRAG Docker Images](https://github.com/HKUDS/LightRAG/pkgs/container/lightrag)
-### Install LightRAG Core
+### Install LightRAG Core
-* Install from source (Recommend)
+- Install from source (Recommend)
```bash
cd LightRAG
pip install -e .
```
-* Install from PyPI
+- Install from PyPI
```bash
pip install lightrag-hku
@@ -157,7 +159,7 @@ LightRAG's demands on the capabilities of Large Language Models (LLMs) are signi
### Quick Start for LightRAG Server
-* For more information about LightRAG Server, please refer to [LightRAG Server](./lightrag/api/README.md).
+- For more information about LightRAG Server, please refer to [LightRAG Server](./lightrag/api/README.md).
### Quick Start for LightRAG core
@@ -180,6 +182,50 @@ For a streaming response implementation example, please see `examples/lightrag_o
**Note 2**: Only `lightrag_openai_demo.py` and `lightrag_openai_compatible_demo.py` are officially supported sample codes. Other sample files are community contributions that haven't undergone full testing and optimization.
+## 🧪 Testing
+
+LightRAG includes a comprehensive test suite with bilingual support (English/Chinese) for testing various graph storage backends.
+
+### Quick Testing
+
+Run the interactive test suite with bilingual support:
+
+```bash
+# Interactive CLI (starts with bilingual language selection)
+python run_tests.py
+
+# Quick mode - run all tests with English interface
+python run_tests.py --language english --storage NetworkXStorage --tests all
+
+# Test specific functionality
+python run_tests.py --language chinese --storage KuzuDBStorage --tests basic advanced
+```
+
+### Available Test Categories
+
+- **Basic Tests**: Node insertion, edge creation, basic graph operations
+- **Advanced Tests**: Complex graph structures, multi-hop relationships
+- **Batch Tests**: Bulk operations, transaction handling
+- **Special Character Tests**: Unicode support, special character encoding
+- **Undirected Graph Tests**: Bidirectional relationships, graph consistency
+
+### Storage Backend Testing
+
+Test with different storage backends:
+
+```bash
+# NetworkX (default, no setup required)
+python run_tests.py --storage NetworkXStorage --tests all
+
+# KuzuDB (high-performance graph database)
+python run_tests.py --storage KuzuDBStorage --tests all
+
+# Neo4j (requires Neo4j instance)
+python run_tests.py --storage Neo4JStorage --tests all
+```
+
+For detailed testing information, see [tests/README.md](tests/README.md).
+
## Programing with LightRAG Core
> ⚠️ **If you would like to integrate LightRAG into your project, we recommend utilizing the REST API provided by the LightRAG Server**. LightRAG Core is typically intended for embedded applications or for researchers who wish to conduct studies and evaluations.
@@ -365,7 +411,7 @@ class QueryParam:
"""
```
-> default value of Top_k can be change by environment variables TOP_K.
+> default value of Top_k can be change by environment variables TOP_K.
### LLM and Embedding Injection
@@ -374,7 +420,7 @@ LightRAG requires the utilization of LLM and Embedding models to accomplish docu
Using Open AI-like APIs
-* LightRAG also supports Open AI-like chat/embeddings APIs:
+- LightRAG also supports Open AI-like chat/embeddings APIs:
```python
async def llm_model_func(
@@ -419,7 +465,7 @@ async def initialize_rag():
Using Hugging Face Models
-* If you want to use Hugging Face models, you only need to set LightRAG as follows:
+- If you want to use Hugging Face models, you only need to set LightRAG as follows:
See `lightrag_hf_demo.py`
@@ -468,11 +514,11 @@ rag = LightRAG(
)
```
-* **Increasing context size**
+- **Increasing context size**
In order for LightRAG to work context should be at least 32k tokens. By default Ollama models have context size of 8k. You can achieve this using one of two ways:
-* **Increasing the `num_ctx` parameter in Modelfile**
+- **Increasing the `num_ctx` parameter in Modelfile**
1. Pull the model:
@@ -498,7 +544,7 @@ PARAMETER num_ctx 32768
ollama create -f Modelfile qwen2m
```
-* **Setup `num_ctx` via Ollama API**
+- **Setup `num_ctx` via Ollama API**
Tiy can use `llm_model_kwargs` param to configure ollama:
@@ -519,7 +565,7 @@ rag = LightRAG(
)
```
-* **Low RAM GPUs**
+- **Low RAM GPUs**
In order to run this experiment on low RAM GPU you should select small model and tune context window (increasing context increase memory consumption). For example, running this ollama example on repurposed mining GPU with 6Gb of RAM required to set context size to 26k while using `gemma2:2b`. It was able to find 197 entities and 19 relations on `book.txt`.
@@ -784,9 +830,9 @@ Example connection configurations for each storage type can be found in the `env
Using Neo4J Storage
-* For production level scenarios you will most likely want to leverage an enterprise solution
-* for KG storage. Running Neo4J in Docker is recommended for seamless local testing.
-* See: https://hub.docker.com/_/neo4j
+- For production level scenarios you will most likely want to leverage an enterprise solution
+- for KG storage. Running Neo4J in Docker is recommended for seamless local testing.
+- See: https://hub.docker.com/_/neo4j
```python
export NEO4J_URI="neo4j://localhost:7687"
@@ -872,9 +918,9 @@ rag = LightRAG(
Using Memgraph for Storage
-* Memgraph is a high-performance, in-memory graph database compatible with the Neo4j Bolt protocol.
-* You can run Memgraph locally using Docker for easy testing:
-* See: https://memgraph.com/download
+- Memgraph is a high-performance, in-memory graph database compatible with the Neo4j Bolt protocol.
+- You can run Memgraph locally using Docker for easy testing:
+- See: https://memgraph.com/download
```python
export MEMGRAPH_URI="bolt://localhost:7687"
@@ -1074,7 +1120,6 @@ rag.insert_custom_kg(custom_kg)
- **create_entity**: Creates a new entity with specified attributes
- **edit_entity**: Updates an existing entity's attributes or renames it
-
- **create_relation**: Creates a new relation between existing entities
- **edit_relation**: Updates an existing relation's attributes
@@ -1100,6 +1145,7 @@ await rag.adelete_by_entity("Google")
```
When deleting an entity:
+
- Removes the entity node from the knowledge graph
- Deletes all associated relationships
- Removes related embedding vectors from the vector database
@@ -1121,6 +1167,7 @@ await rag.adelete_by_relation("Google", "Gmail")
```
When deleting a relationship:
+
- Removes the specified relationship edge
- Deletes the relationship's embedding vector from the vector database
- Preserves both entity nodes and their other relationships
@@ -1138,12 +1185,14 @@ await rag.adelete_by_doc_id("doc-12345")
```
Optimized processing when deleting by document ID:
+
- **Smart Cleanup**: Automatically identifies and removes entities and relationships that belong only to this document
- **Preserve Shared Knowledge**: If entities or relationships exist in other documents, they are preserved and their descriptions are rebuilt
- **Cache Optimization**: Clears related LLM cache to reduce storage overhead
- **Incremental Rebuilding**: Reconstructs affected entity and relationship descriptions from remaining documents
The deletion process includes:
+
1. Delete all text chunks related to the document
2. Identify and delete entities and relationships that belong only to this document
3. Rebuild entities and relationships that still exist in other documents
@@ -1162,6 +1211,7 @@ Note: Deletion by document ID is an asynchronous operation as it involves comple
4. **Backup Recommendations**: Consider backing up data before performing important deletion operations
**Batch Deletion Recommendations:**
+
- For batch deletion operations, consider using asynchronous methods for better performance
- For large-scale deletions, consider processing in batches to avoid excessive system load
@@ -1228,11 +1278,11 @@ rag.merge_entities(
When merging entities:
-* All relationships from source entities are redirected to the target entity
-* Duplicate relationships are intelligently merged
-* Self-relationships (loops) are prevented
-* Source entities are removed after merging
-* Relationship weights and attributes are preserved
+- All relationships from source entities are redirected to the target entity
+- Duplicate relationships are intelligently merged
+- Self-relationships (loops) are prevented
+- Source entities are removed after merging
+- Relationship weights and attributes are preserved
@@ -1241,6 +1291,7 @@ When merging entities:
LightRAG now seamlessly integrates with [RAG-Anything](https://github.com/HKUDS/RAG-Anything), a comprehensive **All-in-One Multimodal Document Processing RAG system** built specifically for LightRAG. RAG-Anything enables advanced parsing and retrieval-augmented generation (RAG) capabilities, allowing you to handle multimodal documents seamlessly and extract structured content—including text, images, tables, and formulas—from various document formats for integration into your RAG pipeline.
**Key Features:**
+
- **End-to-End Multimodal Pipeline**: Complete workflow from document ingestion and parsing to intelligent multimodal query answering
- **Universal Document Support**: Seamless processing of PDFs, Office documents (DOC/DOCX/PPT/PPTX/XLS/XLSX), images, and diverse file formats
- **Specialized Content Analysis**: Dedicated processors for images, tables, mathematical equations, and heterogeneous content types
@@ -1248,31 +1299,32 @@ LightRAG now seamlessly integrates with [RAG-Anything](https://github.com/HKUDS/
- **Hybrid Intelligent Retrieval**: Advanced search capabilities spanning textual and multimodal content with contextual understanding
**Quick Start:**
+
1. Install RAG-Anything:
```bash
pip install raganything
```
2. Process multimodal documents:
-
- RAGAnything Usage Example
-
- ```python
- import asyncio
- from raganything import RAGAnything
- from lightrag import LightRAG
- from lightrag.llm.openai import openai_complete_if_cache, openai_embed
- from lightrag.utils import EmbeddingFunc
- import os
-
- async def load_existing_lightrag():
- # First, create or load an existing LightRAG instance
- lightrag_working_dir = "./existing_lightrag_storage"
-
- # Check if previous LightRAG instance exists
- if os.path.exists(lightrag_working_dir) and os.listdir(lightrag_working_dir):
- print("✅ Found existing LightRAG instance, loading...")
- else:
- print("❌ No existing LightRAG instance found, will create new one")
+
+ RAGAnything Usage Example
+
+ ```python
+ import asyncio
+ from raganything import RAGAnything
+ from lightrag import LightRAG
+ from lightrag.llm.openai import openai_complete_if_cache, openai_embed
+ from lightrag.utils import EmbeddingFunc
+ import os
+
+ async def load_existing_lightrag():
+ # First, create or load an existing LightRAG instance
+ lightrag_working_dir = "./existing_lightrag_storage"
+
+ # Check if previous LightRAG instance exists
+ if os.path.exists(lightrag_working_dir) and os.listdir(lightrag_working_dir):
+ print("✅ Found existing LightRAG instance, loading...")
+ else:
+ print("❌ No existing LightRAG instance found, will create new one")
# Create/Load LightRAG instance with your configurations
lightrag_instance = LightRAG(
@@ -1296,55 +1348,56 @@ LightRAG now seamlessly integrates with [RAG-Anything](https://github.com/HKUDS/
)
)
- # Initialize storage (this will load existing data if available)
- await lightrag_instance.initialize_storages()
-
- # Now initialize RAGAnything with the existing LightRAG instance
- rag = RAGAnything(
- lightrag=lightrag_instance, # Pass the existing LightRAG instance
- # Only need vision model for multimodal processing
- vision_model_func=lambda prompt, system_prompt=None, history_messages=[], image_data=None, **kwargs: openai_complete_if_cache(
- "gpt-4o",
- "",
- system_prompt=None,
- history_messages=[],
- messages=[
- {"role": "system", "content": system_prompt} if system_prompt else None,
- {"role": "user", "content": [
- {"type": "text", "text": prompt},
- {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_data}"}}
- ]} if image_data else {"role": "user", "content": prompt}
- ],
- api_key="your-api-key",
- **kwargs,
- ) if image_data else openai_complete_if_cache(
- "gpt-4o-mini",
- prompt,
- system_prompt=system_prompt,
- history_messages=history_messages,
- api_key="your-api-key",
- **kwargs,
- )
- # Note: working_dir, llm_model_func, embedding_func, etc. are inherited from lightrag_instance
- )
-
- # Query the existing knowledge base
- result = await rag.query_with_multimodal(
- "What data has been processed in this LightRAG instance?",
- mode="hybrid"
- )
- print("Query result:", result)
-
- # Add new multimodal documents to the existing LightRAG instance
- await rag.process_document_complete(
- file_path="path/to/new/multimodal_document.pdf",
- output_dir="./output"
- )
+ # Initialize storage (this will load existing data if available)
+ await lightrag_instance.initialize_storages()
+
+ # Now initialize RAGAnything with the existing LightRAG instance
+ rag = RAGAnything(
+ lightrag=lightrag_instance, # Pass the existing LightRAG instance
+ # Only need vision model for multimodal processing
+ vision_model_func=lambda prompt, system_prompt=None, history_messages=[], image_data=None, **kwargs: openai_complete_if_cache(
+ "gpt-4o",
+ "",
+ system_prompt=None,
+ history_messages=[],
+ messages=[
+ {"role": "system", "content": system_prompt} if system_prompt else None,
+ {"role": "user", "content": [
+ {"type": "text", "text": prompt},
+ {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_data}"}}
+ ]} if image_data else {"role": "user", "content": prompt}
+ ],
+ api_key="your-api-key",
+ **kwargs,
+ ) if image_data else openai_complete_if_cache(
+ "gpt-4o-mini",
+ prompt,
+ system_prompt=system_prompt,
+ history_messages=history_messages,
+ api_key="your-api-key",
+ **kwargs,
+ )
+ # Note: working_dir, llm_model_func, embedding_func, etc. are inherited from lightrag_instance
+ )
+
+ # Query the existing knowledge base
+ result = await rag.query_with_multimodal(
+ "What data has been processed in this LightRAG instance?",
+ mode="hybrid"
+ )
+ print("Query result:", result)
+
+ # Add new multimodal documents to the existing LightRAG instance
+ await rag.process_document_complete(
+ file_path="path/to/new/multimodal_document.pdf",
+ output_dir="./output"
+ )
+
+ if __name__ == "__main__":
+ asyncio.run(load_existing_lightrag())
+ ```
- if __name__ == "__main__":
- asyncio.run(load_existing_lightrag())
- ```
-
+
For detailed documentation and advanced usage, please refer to the [RAG-Anything repository](https://github.com/HKUDS/RAG-Anything).
@@ -1383,13 +1436,16 @@ print("Token usage:", token_tracker.get_usage())
```
### Usage Tips
+
- Use context managers for long sessions or batch operations to automatically track all token consumption
- For scenarios requiring segmented statistics, use manual mode and call reset() when appropriate
- Regular checking of token usage helps detect abnormal consumption early
- Actively use this feature during development and testing to optimize production costs
### Practical Examples
+
You can refer to these examples for implementing token tracking:
+
- `examples/lightrag_gemini_track_token_demo.py`: Token tracking example using Google Gemini model
- `examples/lightrag_siliconcloud_track_token_demo.py`: Token tracking example using SiliconCloud model
@@ -1434,6 +1490,7 @@ rag.export_data("graph_data.md", file_format="md")
# Export data in Text
rag.export_data("graph_data.txt", file_format="txt")
```
+
@@ -1444,15 +1501,16 @@ Include vector embeddings in the export (optional):
```python
rag.export_data("complete_data.csv", include_vector_data=True)
```
+
### Data Included in Export
All exports include:
-* Entity information (names, IDs, metadata)
-* Relation data (connections between entities)
-* Relationship information from vector database
+- Entity information (names, IDs, metadata)
+- Relation data (connections between entities)
+- Relationship information from vector database
## Cache
@@ -1496,10 +1554,12 @@ Valid modes are:
If you encounter these errors when using LightRAG:
1. **`AttributeError: __aenter__`**
+
- **Cause**: Storage backends not initialized
- **Solution**: Call `await rag.initialize_storages()` after creating the LightRAG instance
2. **`KeyError: 'history_messages'`**
+
- **Cause**: Pipeline status not initialized
- **Solution**: Call `await initialize_pipeline_status()` after initializing storages
@@ -1518,7 +1578,7 @@ When switching between different embedding models, you must clear the data direc
## LightRAG API
-The LightRAG Server is designed to provide Web UI and API support. **For more information about LightRAG Server, please refer to [LightRAG Server](./lightrag/api/README.md).**
+The LightRAG Server is designed to provide Web UI and API support. **For more information about LightRAG Server, please refer to [LightRAG Server](./lightrag/api/README.md).**
## Graph Visualization
@@ -1619,28 +1679,28 @@ Output your evaluation in the following JSON format:
### Overall Performance Table
-| |**Agriculture**| |**CS**| |**Legal**| |**Mix**| |
-|----------------------|---------------|------------|------|------------|---------|------------|-------|------------|
-| |NaiveRAG|**LightRAG**|NaiveRAG|**LightRAG**|NaiveRAG|**LightRAG**|NaiveRAG|**LightRAG**|
-|**Comprehensiveness**|32.4%|**67.6%**|38.4%|**61.6%**|16.4%|**83.6%**|38.8%|**61.2%**|
-|**Diversity**|23.6%|**76.4%**|38.0%|**62.0%**|13.6%|**86.4%**|32.4%|**67.6%**|
-|**Empowerment**|32.4%|**67.6%**|38.8%|**61.2%**|16.4%|**83.6%**|42.8%|**57.2%**|
-|**Overall**|32.4%|**67.6%**|38.8%|**61.2%**|15.2%|**84.8%**|40.0%|**60.0%**|
-| |RQ-RAG|**LightRAG**|RQ-RAG|**LightRAG**|RQ-RAG|**LightRAG**|RQ-RAG|**LightRAG**|
-|**Comprehensiveness**|31.6%|**68.4%**|38.8%|**61.2%**|15.2%|**84.8%**|39.2%|**60.8%**|
-|**Diversity**|29.2%|**70.8%**|39.2%|**60.8%**|11.6%|**88.4%**|30.8%|**69.2%**|
-|**Empowerment**|31.6%|**68.4%**|36.4%|**63.6%**|15.2%|**84.8%**|42.4%|**57.6%**|
-|**Overall**|32.4%|**67.6%**|38.0%|**62.0%**|14.4%|**85.6%**|40.0%|**60.0%**|
-| |HyDE|**LightRAG**|HyDE|**LightRAG**|HyDE|**LightRAG**|HyDE|**LightRAG**|
-|**Comprehensiveness**|26.0%|**74.0%**|41.6%|**58.4%**|26.8%|**73.2%**|40.4%|**59.6%**|
-|**Diversity**|24.0%|**76.0%**|38.8%|**61.2%**|20.0%|**80.0%**|32.4%|**67.6%**|
-|**Empowerment**|25.2%|**74.8%**|40.8%|**59.2%**|26.0%|**74.0%**|46.0%|**54.0%**|
-|**Overall**|24.8%|**75.2%**|41.6%|**58.4%**|26.4%|**73.6%**|42.4%|**57.6%**|
-| |GraphRAG|**LightRAG**|GraphRAG|**LightRAG**|GraphRAG|**LightRAG**|GraphRAG|**LightRAG**|
-|**Comprehensiveness**|45.6%|**54.4%**|48.4%|**51.6%**|48.4%|**51.6%**|**50.4%**|49.6%|
-|**Diversity**|22.8%|**77.2%**|40.8%|**59.2%**|26.4%|**73.6%**|36.0%|**64.0%**|
-|**Empowerment**|41.2%|**58.8%**|45.2%|**54.8%**|43.6%|**56.4%**|**50.8%**|49.2%|
-|**Overall**|45.2%|**54.8%**|48.0%|**52.0%**|47.2%|**52.8%**|**50.4%**|49.6%|
+| | **Agriculture** | | **CS** | | **Legal** | | **Mix** | |
+| --------------------- | --------------- | ------------ | -------- | ------------ | --------- | ------------ | --------- | ------------ |
+| | NaiveRAG | **LightRAG** | NaiveRAG | **LightRAG** | NaiveRAG | **LightRAG** | NaiveRAG | **LightRAG** |
+| **Comprehensiveness** | 32.4% | **67.6%** | 38.4% | **61.6%** | 16.4% | **83.6%** | 38.8% | **61.2%** |
+| **Diversity** | 23.6% | **76.4%** | 38.0% | **62.0%** | 13.6% | **86.4%** | 32.4% | **67.6%** |
+| **Empowerment** | 32.4% | **67.6%** | 38.8% | **61.2%** | 16.4% | **83.6%** | 42.8% | **57.2%** |
+| **Overall** | 32.4% | **67.6%** | 38.8% | **61.2%** | 15.2% | **84.8%** | 40.0% | **60.0%** |
+| | RQ-RAG | **LightRAG** | RQ-RAG | **LightRAG** | RQ-RAG | **LightRAG** | RQ-RAG | **LightRAG** |
+| **Comprehensiveness** | 31.6% | **68.4%** | 38.8% | **61.2%** | 15.2% | **84.8%** | 39.2% | **60.8%** |
+| **Diversity** | 29.2% | **70.8%** | 39.2% | **60.8%** | 11.6% | **88.4%** | 30.8% | **69.2%** |
+| **Empowerment** | 31.6% | **68.4%** | 36.4% | **63.6%** | 15.2% | **84.8%** | 42.4% | **57.6%** |
+| **Overall** | 32.4% | **67.6%** | 38.0% | **62.0%** | 14.4% | **85.6%** | 40.0% | **60.0%** |
+| | HyDE | **LightRAG** | HyDE | **LightRAG** | HyDE | **LightRAG** | HyDE | **LightRAG** |
+| **Comprehensiveness** | 26.0% | **74.0%** | 41.6% | **58.4%** | 26.8% | **73.2%** | 40.4% | **59.6%** |
+| **Diversity** | 24.0% | **76.0%** | 38.8% | **61.2%** | 20.0% | **80.0%** | 32.4% | **67.6%** |
+| **Empowerment** | 25.2% | **74.8%** | 40.8% | **59.2%** | 26.0% | **74.0%** | 46.0% | **54.0%** |
+| **Overall** | 24.8% | **75.2%** | 41.6% | **58.4%** | 26.4% | **73.6%** | 42.4% | **57.6%** |
+| | GraphRAG | **LightRAG** | GraphRAG | **LightRAG** | GraphRAG | **LightRAG** | GraphRAG | **LightRAG** |
+| **Comprehensiveness** | 45.6% | **54.4%** | 48.4% | **51.6%** | 48.4% | **51.6%** | **50.4%** | 49.6% |
+| **Diversity** | 22.8% | **77.2%** | 40.8% | **59.2%** | 26.4% | **73.6%** | 36.0% | **64.0%** |
+| **Empowerment** | 41.2% | **58.8%** | 45.2% | **54.8%** | 43.6% | **56.4%** | **50.8%** | 49.2% |
+| **Overall** | 45.2% | **54.8%** | 48.0% | **52.0%** | 47.2% | **52.8%** | **50.4%** | 49.6% |
## Reproduce
@@ -1783,7 +1843,7 @@ def extract_queries(file_path):
## 🔗 Related Projects
-*Ecosystem & Extensions*
+_Ecosystem & Extensions_
@@ -1845,7 +1905,6 @@ def extract_queries(file_path):
---
-
## 📖 Citation
```python
diff --git a/lightrag/base.py b/lightrag/base.py
index cc8e3c099c..4a87d35fa9 100644
--- a/lightrag/base.py
+++ b/lightrag/base.py
@@ -560,7 +560,9 @@ async def get_edges_by_chunk_ids(self, chunk_ids: list[str]) -> list[dict]:
return all_edges
@abstractmethod
- async def upsert_node(self, node_id: str, node_data: dict[str, str]) -> None:
+ async def upsert_node(
+ self, node_id: str, node_data: dict[str, str | int | float]
+ ) -> None:
"""Insert a new node or update an existing node in the graph.
Importance notes for in-memory storage:
@@ -575,7 +577,10 @@ async def upsert_node(self, node_id: str, node_data: dict[str, str]) -> None:
@abstractmethod
async def upsert_edge(
- self, source_node_id: str, target_node_id: str, edge_data: dict[str, str]
+ self,
+ source_node_id: str,
+ target_node_id: str,
+ edge_data: dict[str, str | int | float],
) -> None:
"""Insert a new edge or update an existing edge in the graph.
diff --git a/lightrag/kg/__init__.py b/lightrag/kg/__init__.py
index 8d42441ac7..64d8abf64f 100644
--- a/lightrag/kg/__init__.py
+++ b/lightrag/kg/__init__.py
@@ -15,6 +15,10 @@
"PGGraphStorage",
"MongoGraphStorage",
"MemgraphStorage",
+ "KuzuDBStorage",
+ # "AGEStorage",
+ # "TiDBGraphStorage",
+ # "GremlinStorage",
],
"required_methods": ["upsert_node", "upsert_edge"],
},
@@ -53,6 +57,8 @@
"Neo4JStorage": ["NEO4J_URI", "NEO4J_USERNAME", "NEO4J_PASSWORD"],
"MongoGraphStorage": [],
"MemgraphStorage": ["MEMGRAPH_URI"],
+ "KuzuDBStorage": [],
+ # "TiDBGraphStorage": ["TIDB_USER", "TIDB_PASSWORD", "TIDB_DATABASE"],
"AGEStorage": [
"AGE_POSTGRES_DB",
"AGE_POSTGRES_USER",
@@ -101,6 +107,7 @@
"FaissVectorDBStorage": ".kg.faiss_impl",
"QdrantVectorDBStorage": ".kg.qdrant_impl",
"MemgraphStorage": ".kg.memgraph_impl",
+ "KuzuDBStorage": ".kg.kuzu_impl",
}
diff --git a/lightrag/kg/kuzu_impl.py b/lightrag/kg/kuzu_impl.py
new file mode 100644
index 0000000000..313af53d5c
--- /dev/null
+++ b/lightrag/kg/kuzu_impl.py
@@ -0,0 +1,1032 @@
+import os
+from dataclasses import dataclass
+from typing import final, cast
+from collections import deque
+
+from ..utils import logger
+from ..base import BaseGraphStorage
+from ..types import KnowledgeGraph, KnowledgeGraphNode, KnowledgeGraphEdge
+
+import kuzu
+
+
+@final
+@dataclass
+class KuzuDBStorage(BaseGraphStorage):
+ def __init__(self, namespace, global_config, embedding_func, workspace=None):
+ # Check KUZU_WORKSPACE environment variable and override workspace if set
+ kuzu_workspace = os.environ.get("KUZU_WORKSPACE")
+ if kuzu_workspace and kuzu_workspace.strip():
+ workspace = kuzu_workspace
+
+ super().__init__(
+ namespace=namespace,
+ workspace=workspace or "base",
+ global_config=global_config,
+ embedding_func=embedding_func,
+ )
+ self._db = None
+ self._conn = None
+
+ def _get_label(self) -> str:
+ """Get workspace label, return 'base' for compatibility when workspace is empty"""
+ workspace = getattr(self, "workspace", None)
+ return workspace if workspace else "base"
+
+ @property
+ def connection(self) -> kuzu.Connection:
+ """Get connection with type safety guarantee"""
+ if self._conn is None:
+ raise RuntimeError(
+ "Database connection is not initialized. Call initialize() first."
+ )
+ return cast(kuzu.Connection, self._conn)
+
+ def get_first(self, result) -> kuzu.QueryResult:
+ """Normalize query result to handle both single QueryResult and list[QueryResult] cases"""
+ if isinstance(result, list):
+ if len(result) == 0:
+ raise RuntimeError("Query returned empty result list")
+ return result[0] # Take the first result
+ return result
+
+ def get_all(self, result) -> list[kuzu.QueryResult]:
+ """Get all query results, handling both single QueryResult and list[QueryResult] cases"""
+ if isinstance(result, list):
+ return result
+ return [result]
+
+ async def initialize(self):
+ if self._conn is not None:
+ # Already initialized
+ return
+
+ db_path = os.environ.get("KUZU_DB_PATH", f"kuzu_db_{self.namespace}")
+
+ # Initialize the KuzuDB instance
+ self._db = kuzu.Database(db_path)
+ self._conn = kuzu.Connection(self._db)
+
+ # Create node and relationship tables if they don't exist
+ label = self._get_label()
+
+ try:
+ # Create node table with flexible schema
+ self._conn.execute(
+ f"""
+ CREATE NODE TABLE IF NOT EXISTS {label}(
+ entity_id STRING,
+ entity_type STRING,
+ description STRING,
+ keywords STRING,
+ source_id STRING,
+ PRIMARY KEY(entity_id)
+ )
+ """
+ )
+
+ # Create relationship table
+ self._conn.execute(
+ f"""
+ CREATE REL TABLE IF NOT EXISTS Related(
+ FROM {label} TO {label},
+ relationship STRING,
+ weight DOUBLE,
+ description STRING,
+ keywords STRING,
+ source_id STRING
+ )
+ """
+ )
+
+ logger.info(f"KuzuDB initialized at {db_path}")
+
+ except Exception as e:
+ logger.error(f"Error initializing KuzuDB: {str(e)}")
+ raise
+
+ async def finalize(self):
+ """Close the KuzuDB connection and release all resources"""
+ if self._conn:
+ self._conn.close()
+ self._conn = None
+ self._db = None
+
+ async def __aexit__(self, exc_type, exc, tb):
+ """Ensure connection is closed when context manager exits"""
+ await self.finalize()
+
+ async def index_done_callback(self) -> None:
+ # KuzuDB handles persistence automatically
+ pass
+
+ async def has_node(self, node_id: str) -> bool:
+ """Check if a node with the given entity_id exists in the database"""
+ label = self._get_label()
+ try:
+ query = f"MATCH (n:{label}) WHERE n.entity_id = $entity_id RETURN n"
+ result = self.get_first(
+ self.connection.execute(query, {"entity_id": node_id})
+ )
+ return result.has_next()
+ except Exception as e:
+ logger.error(f"Error checking node existence for {node_id}: {str(e)}")
+ return False
+
+ async def has_edge(self, source_node_id: str, target_node_id: str) -> bool:
+ """Check if an edge exists between two nodes"""
+ label = self._get_label()
+ try:
+ query = f"""
+ MATCH (a:{label})-[r:Related]-(b:{label})
+ WHERE a.entity_id = $source_id AND b.entity_id = $target_id
+ RETURN r
+ """
+ result = self.get_first(
+ self.connection.execute(
+ query, {"source_id": source_node_id, "target_id": target_node_id}
+ )
+ )
+ return result.has_next()
+ except Exception as e:
+ logger.error(f"Error checking edge existence: {str(e)}")
+ return False
+
+ async def get_node(self, node_id: str) -> dict[str, str] | None:
+ """Get node by its entity_id identifier, return only node properties"""
+ label = self._get_label()
+ try:
+ query = f"MATCH (n:{label}) WHERE n.entity_id = $entity_id RETURN n.*"
+ result = self.get_first(
+ self.connection.execute(query, {"entity_id": node_id})
+ )
+
+ if result.has_next():
+ row = result.get_next()
+ node_dict = {
+ "entity_id": row[0],
+ "entity_type": row[1],
+ "description": row[2],
+ "keywords": row[3],
+ "source_id": row[4],
+ }
+ return node_dict
+ return None
+ except Exception as e:
+ logger.error(f"Error getting node for {node_id}: {str(e)}")
+ return None
+
+ async def get_nodes_batch(self, node_ids: list[str]) -> dict[str, dict]:
+ """Retrieve multiple nodes in one query"""
+ label = self._get_label()
+ query = f"""
+ UNWIND $node_ids AS id
+ MATCH (n:{label} {{entity_id: id}})
+ RETURN n.entity_id AS entity_id, n
+ """
+
+ result = self.get_all(self.connection.execute(query, {"node_ids": node_ids}))
+ nodes = {}
+
+ # for node_id in node_ids:
+ # node = await self.get_node(node_id)
+ # if node is not None:
+ # nodes[node_id] = node
+
+ for query_result in result:
+ if not query_result.has_next():
+ continue
+ while query_result.has_next():
+ row = query_result.get_next()
+ node_id = row[0]
+ node_data = {
+ "entity_id": row[1].get("entity_id"),
+ "entity_type": row[1].get("entity_type"),
+ "description": row[1].get("description"),
+ "keywords": row[1].get("keywords"),
+ "source_id": row[1].get("source_id"),
+ }
+ nodes[node_id] = node_data
+ return nodes
+
+ async def node_degree(self, node_id: str) -> int:
+ """Get the degree (number of relationships) of a node"""
+ label = self._get_label()
+ try:
+ query = f"""
+ MATCH (n:{label})
+ WHERE n.entity_id = $entity_id
+ OPTIONAL MATCH (n)-[r:Related]-()
+ RETURN COUNT(r) AS degree
+ """
+ result = self.get_first(
+ self.connection.execute(query, {"entity_id": node_id})
+ )
+
+ if result.has_next():
+ row = result.get_next()
+ # Since we create bidirectional edges, divide by 2 to get the actual degree
+ degree = row[0] if row[0] is not None else 0
+ return degree // 2
+ return 0
+ except Exception as e:
+ logger.error(f"Error getting node degree for {node_id}: {str(e)}")
+ return 0
+
+ async def node_degrees_batch(self, node_ids: list[str]) -> dict[str, int]:
+ """Retrieve the degree for multiple nodes"""
+ label = self._get_label()
+
+ query = f"""
+ UNWIND $node_ids AS id
+ MATCH (n:{label} {{entity_id: id}})
+ OPTIONAL MATCH (n)-[r:Related]-()
+ RETURN n.entity_id AS entity_id, COUNT(r) AS degree
+ """
+ result = self.get_all(self.connection.execute(query, {"node_ids": node_ids}))
+
+ degrees = {}
+ for query_result in result:
+ if not query_result.has_next():
+ continue
+ while query_result.has_next():
+ row = query_result.get_next()
+ node_id = row[0]
+ degree = row[1] if row[1] is not None else 0
+ degrees[node_id] = degree // 2 # Divide by 2 for bidirectional edges
+ return degrees
+
+ async def edge_degree(self, src_id: str, tgt_id: str) -> int:
+ """Get the total degree (sum of relationships) of two nodes"""
+ src_degree = await self.node_degree(src_id)
+ tgt_degree = await self.node_degree(tgt_id)
+ return src_degree + tgt_degree
+
+ async def edge_degrees_batch(
+ self, edge_pairs: list[tuple[str, str]]
+ ) -> dict[tuple[str, str], int]:
+ """Calculate the combined degree for each edge"""
+ unique_node_ids = set()
+ for src, tgt in edge_pairs:
+ unique_node_ids.add(src)
+ unique_node_ids.add(tgt)
+
+ degrees = await self.node_degrees_batch(list(unique_node_ids))
+
+ edge_degrees = {}
+ for src, tgt in edge_pairs:
+ edge_degrees[(src, tgt)] = degrees.get(src, 0) + degrees.get(tgt, 0)
+ return edge_degrees
+
+ async def get_edge(
+ self, source_node_id: str, target_node_id: str
+ ) -> dict[str, str | float | None] | None:
+ """Get edge properties between two nodes"""
+ label = self._get_label()
+ try:
+ query = f"""
+ MATCH (a:{label})-[r:Related]-(b:{label})
+ WHERE a.entity_id = $source_id AND b.entity_id = $target_id
+ RETURN r.*
+ """
+ result = self.get_first(
+ self.connection.execute(
+ query, {"source_id": source_node_id, "target_id": target_node_id}
+ )
+ )
+
+ if result.has_next():
+ row = result.get_next()
+ column_names = result.get_column_names()
+ edge_dict = dict(zip(column_names, row))
+
+ # Remove the 'r.' prefix from column names for cleaner keys
+ clean_edge_dict = {}
+ for key, value in edge_dict.items():
+ clean_key = key.replace("r.", "") if key.startswith("r.") else key
+ clean_edge_dict[clean_key] = value
+
+ # Ensure required keys exist with defaults
+ required_keys = {
+ "relationship": None,
+ "weight": 0.0,
+ "source_id": None,
+ "description": None,
+ "keywords": None,
+ }
+ for key, default_value in required_keys.items():
+ if key not in clean_edge_dict:
+ clean_edge_dict[key] = default_value
+
+ return clean_edge_dict
+ return None
+ except Exception as e:
+ logger.error(
+ f"Error getting edge between {source_node_id} and {target_node_id}: {str(e)}"
+ )
+ return None
+
+ async def get_edges_batch(
+ self, pairs: list[dict[str, str]]
+ ) -> dict[tuple[str, str], dict]:
+ """Retrieve edge properties for multiple (src, tgt) pairs"""
+ label = self._get_label()
+ query = f"""
+ UNWIND $pairs AS pair
+ MATCH (a:{label})-[r:Related]-(b:{label})
+ WHERE a.entity_id = pair.src AND b.entity_id = pair.tgt
+ RETURN pair.src AS src_id, pair.tgt AS tgt_id, collect(r)
+ """
+ result = self.get_all(self.connection.execute(query, {"pairs": pairs}))
+ edges_dict = {}
+ for pair in result:
+ if not pair.has_next():
+ continue
+ while pair.has_next():
+ row = pair.get_next()
+ # print("🔥 ROW:", row)
+ src_id = row[0] # "Deep Learning"
+ tgt_id = row[1] # "Natural Language Processing"
+ edges = row[2] # List of edges
+ if edges:
+ for edge in edges:
+ edges_dict[(src_id, tgt_id)] = {
+ "relationship": edge.get("relationship"),
+ "weight": edge.get("weight", 0.0),
+ "source_id": edge.get("source_id"),
+ "description": edge.get("description"),
+ "keywords": edge.get("keywords"),
+ }
+ else:
+ edges_dict[(src_id, tgt_id)] = {
+ "relationship": None,
+ "weight": 0.0,
+ "source_id": None,
+ "description": None,
+ "keywords": None,
+ }
+ return edges_dict
+
+ async def get_node_edges(self, source_node_id: str) -> list[tuple[str, str]] | None:
+ """Retrieves all edges for a particular node"""
+ label = self._get_label()
+ try:
+ query = f"""
+ MATCH (n:{label})-[r:Related]-(connected:{label})
+ WHERE n.entity_id = $entity_id
+ RETURN n.entity_id, connected.entity_id
+ """
+ result = self.get_all(
+ self.connection.execute(query, {"entity_id": source_node_id})
+ )
+
+ edges = []
+ seen_pairs = set()
+ for result in result:
+ if not result.has_next():
+ continue
+ while result.has_next():
+ row = result.get_next()
+ # Create a normalized edge pair to avoid duplicates
+ node1, node2 = row[0], row[1]
+ # Sort the pair to ensure consistent ordering
+ edge_pair = tuple(sorted([node1, node2]))
+ if edge_pair not in seen_pairs:
+ edges.append((node1, node2))
+ seen_pairs.add(edge_pair)
+
+ return edges if edges else None
+ except Exception as e:
+ logger.error(f"Error getting edges for node {source_node_id}: {str(e)}")
+ return None
+
+ async def get_nodes_edges_batch(
+ self, node_ids: list[str]
+ ) -> dict[str, list[tuple[str, str]]]:
+ """Batch retrieve edges for multiple nodes"""
+ label = self._get_label()
+ result = {}
+ query = f"""
+ UNWIND $node_ids AS id
+ MATCH (n:`{label}` {{entity_id: id}})
+ OPTIONAL MATCH (n)-[r]-(connected:`{label}`)
+ RETURN id, connected.entity_id
+ """
+ result = self.get_all(self.connection.execute(query, {"node_ids": node_ids}))
+ edges_dict = {}
+ for r in result:
+ if not r.has_next():
+ continue
+ while r.has_next():
+ row = r.get_next()
+ # print("🔥 row: ", row)
+ queried_id = row[0]
+ if not queried_id:
+ continue
+
+ if queried_id not in edges_dict:
+ edges_dict[queried_id] = []
+ # deduplicate edges
+ edge_pair = (queried_id, row[1])
+ if edge_pair not in edges_dict[queried_id]:
+ edges_dict[queried_id].append(edge_pair)
+
+ # Ensure all queried nodes have an entry in the dictionary
+ return edges_dict
+
+ async def get_nodes_by_chunk_ids(self, chunk_ids: list[str]) -> list[dict]:
+ """Get all nodes that are associated with the given chunk_ids"""
+ label = self._get_label()
+ nodes = []
+ seen_nodes = set() # Track seen entity_ids to avoid duplicates
+
+ try:
+ for chunk_id in chunk_ids:
+ query = f"""
+ MATCH (n:{label})
+ WHERE n.source_id CONTAINS $chunk_id
+ RETURN n.*
+ """
+ result = self.get_all(
+ self.connection.execute(query, {"chunk_id": chunk_id})
+ )
+
+ for result in result:
+ if not result.has_next():
+ continue
+ while result.has_next():
+ row = result.get_next()
+ # print("🔥 ROW:", row)
+ column_names = result.get_column_names()
+ # print("🔥 Column names:", column_names)
+ node_dict = dict(zip(column_names, row))
+ # print("🔥 Node dict:", node_dict)
+ # Clean up column names by removing prefixes
+ clean_node_dict = {}
+ for key, value in node_dict.items():
+ clean_key = (
+ key.replace("n.", "") if key.startswith("n.") else key
+ )
+ clean_node_dict[clean_key] = value
+
+ clean_node_dict["id"] = clean_node_dict.get("entity_id")
+
+ # Only add the node if we haven't seen it before
+ entity_id = clean_node_dict.get("entity_id")
+ if entity_id and entity_id not in seen_nodes:
+ nodes.append(clean_node_dict)
+ seen_nodes.add(entity_id)
+ except Exception as e:
+ logger.error(f"Error getting nodes by chunk ids: {str(e)}")
+ # print("🔥 Nodes found:", nodes)
+ return nodes
+
+ async def get_edges_by_chunk_ids(self, chunk_ids: list[str]) -> list[dict]:
+ """Get all edges that are associated with the given chunk_ids"""
+ label = self._get_label()
+ edges = []
+ seen_edges = set() # Track seen edge pairs to avoid duplicates
+
+ try:
+ for chunk_id in chunk_ids:
+ query = f"""
+ MATCH (a:{label})-[r:Related]-(b:{label})
+ WHERE r.source_id CONTAINS $chunk_id
+ RETURN a.entity_id, b.entity_id, r.*
+ """
+ result = self.get_all(
+ self.connection.execute(query, {"chunk_id": chunk_id})
+ )
+ for result in result:
+ if not result.has_next():
+ continue
+ # Process each result
+ # Note: KuzuDB returns results in a QueryResult object
+ # We need to iterate through it to get the actual rows
+ while result.has_next():
+ row = result.get_next()
+ column_names = result.get_column_names()
+ edge_dict = dict(
+ zip(column_names[2:], row[2:])
+ ) # Skip source and target
+
+ # Clean up column names by removing prefixes
+ clean_edge_dict = {}
+ for key, value in edge_dict.items():
+ clean_key = (
+ key.replace("r.", "") if key.startswith("r.") else key
+ )
+ clean_edge_dict[clean_key] = value
+
+ source, target = row[0], row[1]
+ clean_edge_dict["source"] = source
+ clean_edge_dict["target"] = target
+
+ # Create normalized edge pair to avoid bidirectional duplicates
+ edge_pair = tuple(sorted([source, target]))
+ if edge_pair not in seen_edges:
+ edges.append(clean_edge_dict)
+ seen_edges.add(edge_pair)
+ except Exception as e:
+ logger.error(f"Error getting edges by chunk ids: {str(e)}")
+
+ return edges
+
+ async def upsert_node(self, node_id: str, node_data: dict[str, str]) -> None:
+ """Upsert a node in the KuzuDB database"""
+ label = self._get_label()
+ # print(f"🔥 Upserting node {node_id} with data: {node_data}")
+ if "entity_id" not in node_data:
+ raise ValueError(
+ "KuzuDB: node properties must contain an 'entity_id' field"
+ )
+
+ try:
+ # Build the properties for the MERGE statement
+ params = {"entity_id": node_id}
+ set_props = []
+
+ for key, value in node_data.items():
+ if key == "entity_id":
+ # entity_id is used in MERGE condition, skip it
+ continue
+ else:
+ param_name = f"prop_{key}"
+ params[param_name] = value
+ set_props.append(f"n.{key} = ${param_name}")
+
+ set_clause = ", ".join(set_props) if set_props else ""
+
+ if set_clause:
+ query = f"""
+ MERGE (n:{label} {{entity_id: $entity_id}})
+ SET {set_clause}
+ """
+ else:
+ query = f"""
+ MERGE (n:{label} {{entity_id: $entity_id}})
+ """
+
+ self.connection.execute(query, params)
+
+ except Exception as e:
+ logger.error(f"Error during node upsert: {str(e)}")
+ raise
+
+ async def upsert_edge(
+ self,
+ source_node_id: str,
+ target_node_id: str,
+ edge_data: dict[str, str | int | float],
+ ) -> None:
+ """Upsert an edge and its properties between two nodes"""
+ label = self._get_label()
+
+ try:
+ # Build the properties dict
+ params: dict[str, str | int | float] = {
+ "source_id": source_node_id,
+ "target_id": target_node_id,
+ }
+ set_props = []
+
+ for key, value in edge_data.items():
+ param_name = f"prop_{key}"
+ params[param_name] = value
+ set_props.append(f"r.{key} = ${param_name}")
+
+ set_clause = f"SET {', '.join(set_props)}" if set_props else ""
+
+ # Create bidirectional edges to simulate unRelated behavior
+ # Edge 1: source -> target
+ query1 = f"""
+ MATCH (source:{label} {{entity_id: $source_id}})
+ MATCH (target:{label} {{entity_id: $target_id}})
+ MERGE (source)-[r:Related]->(target)
+ {set_clause}
+ """
+
+ # Edge 2: target -> source
+ query2 = f"""
+ MATCH (source:{label} {{entity_id: $source_id}})
+ MATCH (target:{label} {{entity_id: $target_id}})
+ MERGE (target)-[r:Related]->(source)
+ {set_clause}
+ """
+
+ self.connection.execute(query1, params)
+ self.connection.execute(query2, params)
+
+ except Exception as e:
+ logger.error(f"Error during edge upsert: {str(e)}")
+ raise
+
+ async def get_knowledge_graph(
+ self, node_label: str, max_depth: int = 3, max_nodes: int | None = None
+ ) -> KnowledgeGraph:
+ """Retrieve a connected subgraph of nodes"""
+ if max_nodes is None:
+ max_nodes = self.global_config.get("max_graph_nodes", 1000)
+ else:
+ max_nodes = min(max_nodes, self.global_config.get("max_graph_nodes", 1000))
+
+ label = self._get_label()
+ result = KnowledgeGraph()
+
+ if node_label == "*":
+ # Get all nodes with highest degree (accounting for bidirectional edges)
+ try:
+ query = f"""
+ MATCH (n:{label})
+ OPTIONAL MATCH (n)-[r:Related]-(connected:{label})
+ WITH n, COUNT(DISTINCT connected) AS degree
+ RETURN n.*, degree
+ ORDER BY degree DESC, n.entity_id ASC
+ LIMIT $max_nodes
+ """
+
+ node_result = self.get_all(
+ self.connection.execute(query, {"max_nodes": max_nodes})
+ )
+ seen_nodes = set()
+
+ for node_query_result in node_result:
+ if not node_query_result.has_next():
+ continue
+ while node_query_result.has_next():
+ row = node_query_result.get_next()
+ column_names = node_query_result.get_column_names()
+ node_data = dict(
+ zip(column_names[:-1], row[:-1])
+ ) # Exclude degree
+
+ # Clean up column names by removing prefixes
+ clean_node_data = {}
+ for key, value in node_data.items():
+ clean_key = (
+ key.replace("n.", "") if key.startswith("n.") else key
+ )
+ clean_node_data[clean_key] = value
+
+ entity_id = clean_node_data.get("entity_id")
+ if entity_id and entity_id not in seen_nodes:
+ result.nodes.append(
+ KnowledgeGraphNode(
+ id=entity_id,
+ labels=[entity_id],
+ properties=clean_node_data,
+ )
+ )
+ seen_nodes.add(entity_id)
+
+ # Get edges between these nodes
+ if seen_nodes:
+ entity_ids = list(seen_nodes)
+ edges_query = f"""
+ MATCH (a:{label})-[r:Related]-(b:{label})
+ WHERE a.entity_id IN $entity_ids AND b.entity_id IN $entity_ids
+ RETURN a.entity_id, b.entity_id, r.*
+ """
+
+ edge_result = self.get_all(
+ self.connection.execute(edges_query, {"entity_ids": entity_ids})
+ )
+ seen_edges = set()
+
+ for edge_query_result in edge_result:
+ if not edge_query_result.has_next():
+ continue
+ while edge_query_result.has_next():
+ row = edge_query_result.get_next()
+ column_names = edge_query_result.get_column_names()
+ # print("==> ROW:", row)
+ # print("==> COLS:", column_names)
+ edge_data = dict(zip(column_names[2:], row[2:]))
+ clean_edge_data = {}
+ for key, value in edge_data.items():
+ clean_key = (
+ key.replace("r.", "")
+ if key.startswith("r.")
+ else key
+ )
+ clean_edge_data[clean_key] = value
+
+ # Create normalized edge_id to avoid bidirectional duplicates
+ source, target = row[0], row[1]
+ edge_pair = tuple(sorted([source, target]))
+ edge_id = f"{edge_pair[0]}-{edge_pair[1]}"
+
+ if edge_id not in seen_edges:
+ result.edges.append(
+ KnowledgeGraphEdge(
+ id=edge_id,
+ type="UNDIRECTED",
+ source=edge_pair[0],
+ target=edge_pair[1],
+ properties=clean_edge_data,
+ )
+ )
+ seen_edges.add(edge_id)
+
+ except Exception as e:
+ logger.error(f"Error getting knowledge graph: {str(e)}")
+ else:
+ # BFS traversal for specific node
+ # Ensure max_nodes is not None before passing to _bfs_subgraph
+ assert max_nodes is not None
+ return await self._bfs_subgraph(node_label, max_depth, max_nodes)
+ # print("🔥 result:", result)
+ return result
+
+ async def _bfs_subgraph(
+ self, node_label: str, max_depth: int, max_nodes: int
+ ) -> KnowledgeGraph:
+ """BFS implementation for subgraph traversal"""
+
+ result = KnowledgeGraph()
+ visited_nodes = set()
+ visited_edges = set()
+
+ # Get starting node
+ start_node = await self.get_node(node_label)
+ if not start_node:
+ return result
+
+ # Initialize BFS
+ queue = deque([(node_label, 0)])
+
+ while queue and len(visited_nodes) < max_nodes:
+ current_node_id, current_depth = queue.popleft()
+
+ # Skip if already visited or depth exceeded
+ if current_node_id in visited_nodes or current_depth > max_depth:
+ continue
+
+ # Add current node
+ node_data = await self.get_node(current_node_id)
+ if node_data:
+ result.nodes.append(
+ KnowledgeGraphNode(
+ id=current_node_id,
+ labels=[current_node_id],
+ properties=node_data,
+ )
+ )
+ visited_nodes.add(current_node_id)
+
+ # Get neighbors if not at max depth
+ if current_depth < max_depth:
+ edges = await self.get_node_edges(current_node_id)
+ if edges:
+ for source_id, target_id in edges:
+ # Create normalized edge ID to avoid duplicates
+ edge_pair = tuple(sorted([source_id, target_id]))
+ edge_id = f"{edge_pair[0]}-{edge_pair[1]}"
+
+ # Add edge if not already processed
+ if edge_id not in visited_edges:
+ edge_data = await self.get_edge(source_id, target_id)
+ if edge_data:
+ result.edges.append(
+ KnowledgeGraphEdge(
+ id=edge_id,
+ type="UNDIRECTED",
+ source=edge_pair[0],
+ target=edge_pair[1],
+ properties=edge_data,
+ )
+ )
+ visited_edges.add(edge_id)
+
+ # Add unvisited neighbors to queue
+ neighbor_id = (
+ target_id if source_id == current_node_id else source_id
+ )
+ if (
+ neighbor_id not in visited_nodes
+ and len(visited_nodes) < max_nodes
+ ):
+ queue.append((neighbor_id, current_depth + 1))
+
+ # Set truncation flag if we hit the node limit
+ if len(visited_nodes) >= max_nodes:
+ result.is_truncated = True
+
+ return result
+
+ async def get_all_labels(self) -> list[str]:
+ """Get all existing node labels in the database"""
+ label = self._get_label()
+ try:
+ query = f"""
+ MATCH (n:{label})
+ WHERE n.entity_id IS NOT NULL
+ RETURN DISTINCT n.entity_id
+ ORDER BY n.entity_id
+ """
+ result = self.get_all(self.connection.execute(query))
+
+ labels = []
+ for query_result in result:
+ if not query_result.has_next():
+ continue
+ while query_result.has_next():
+ row = query_result.get_next()
+ labels.append(row[0])
+
+ return labels
+ except Exception as e:
+ logger.error(f"Error getting all labels: {str(e)}")
+ return []
+
+ async def delete_node(self, node_id: str) -> None:
+ """Delete a node with the specified entity_id"""
+ label = self._get_label()
+ try:
+ query = f"""
+ MATCH (n:{label} {{entity_id: $entity_id}})
+ DETACH DELETE n
+ """
+ self.connection.execute(query, {"entity_id": node_id})
+ logger.debug(f"Deleted node with entity_id '{node_id}'")
+ except Exception as e:
+ logger.error(f"Error during node deletion: {str(e)}")
+ raise
+
+ async def remove_nodes(self, nodes: list[str]):
+ """Delete multiple nodes"""
+ for node_id in nodes:
+ await self.delete_node(node_id)
+
+ async def remove_edges(self, edges: list[tuple[str, str]]):
+ """Delete multiple edges"""
+ label = self._get_label()
+ for source, target in edges:
+ try:
+ # Since we create bidirectional edges, we need to delete both directions
+ # Delete source -> target
+ query1 = f"""
+ MATCH (source:{label} {{entity_id: $source_id}})-[r:Related]->(target:{label} {{entity_id: $target_id}})
+ DELETE r
+ """
+ self.connection.execute(
+ query1, {"source_id": source, "target_id": target}
+ )
+
+ # Delete target -> source
+ query2 = f"""
+ MATCH (target:{label} {{entity_id: $target_id}})-[r:Related]->(source:{label} {{entity_id: $source_id}})
+ DELETE r
+ """
+ self.connection.execute(
+ query2, {"source_id": source, "target_id": target}
+ )
+
+ logger.debug(
+ f"Deleted bidirectional edge between '{source}' and '{target}'"
+ )
+ except Exception as e:
+ logger.error(f"Error during edge deletion: {str(e)}")
+ raise
+
+ async def drop(self) -> dict[str, str]:
+ """Drop all data from current workspace storage and clean up resources"""
+ label = self._get_label()
+ try:
+ query = f"MATCH (n:{label}) DETACH DELETE n"
+ self.connection.execute(query)
+
+ logger.info(f"Dropped KuzuDB workspace '{label}'")
+ return {
+ "status": "success",
+ "message": f"workspace '{label}' data dropped",
+ }
+ except Exception as e:
+ logger.error(f"Error dropping KuzuDB workspace '{label}': {e}")
+ return {"status": "error", "message": str(e)}
+
+ async def get_all_nodes(self) -> list[dict]:
+ """Get all nodes from the database"""
+ label = self._get_label()
+ try:
+ query = f"MATCH (n:{label}) RETURN n.*"
+ result = self.get_all(self.connection.execute(query))
+
+ nodes = []
+ for query_result in result:
+ if not query_result.has_next():
+ continue
+ while query_result.has_next():
+ row = query_result.get_next()
+ node_dict = {
+ "entity_id": row[0],
+ "entity_type": row[1],
+ "description": row[2],
+ "keywords": row[3],
+ "source_id": row[4],
+ }
+ nodes.append(node_dict)
+ return nodes
+ except Exception as e:
+ logger.error(f"Error getting all nodes: {str(e)}")
+ return []
+
+ async def get_all_edges(self) -> list[dict]:
+ """Get all edges from the database"""
+ label = self._get_label()
+ try:
+ query = f"""
+ MATCH (a:{label})-[r:Related]-(b:{label})
+ RETURN a.entity_id, b.entity_id, r.*
+ """
+ result = self.get_all(self.connection.execute(query))
+
+ edges = []
+ seen_edges = set()
+ for query_result in result:
+ if not query_result.has_next():
+ continue
+ while query_result.has_next():
+ row = query_result.get_next()
+ column_names = query_result.get_column_names()
+ edge_data = dict(zip(column_names[2:], row[2:]))
+
+ # Clean up column names by removing prefixes
+ clean_edge_data = {}
+ for key, value in edge_data.items():
+ clean_key = (
+ key.replace("r.", "") if key.startswith("r.") else key
+ )
+ clean_edge_data[clean_key] = value
+
+ source, target = row[0], row[1]
+ clean_edge_data["source"] = source
+ clean_edge_data["target"] = target
+
+ # Create normalized edge pair to avoid bidirectional duplicates
+ edge_pair = tuple(sorted([source, target]))
+ if edge_pair not in seen_edges:
+ edges.append(clean_edge_data)
+ seen_edges.add(edge_pair)
+
+ return edges
+ except Exception as e:
+ logger.error(f"Error getting all edges: {str(e)}")
+ return []
+
+ async def get_popular_labels(self, top_k: int = 10) -> list[str]:
+ """Get the most popular node labels by degree"""
+ label = self._get_label()
+ try:
+ query = f"""
+ MATCH (n:{label})
+ OPTIONAL MATCH (n)-[r:Related]-(connected:{label})
+ WITH n.entity_id AS entity_id, COUNT(DISTINCT connected) AS degree
+ RETURN entity_id
+ ORDER BY degree DESC, entity_id ASC
+ LIMIT $top_k
+ """
+ result = self.get_all(self.connection.execute(query, {"top_k": top_k}))
+
+ labels = []
+ for query_result in result:
+ if not query_result.has_next():
+ continue
+ while query_result.has_next():
+ row = query_result.get_next()
+ labels.append(row[0])
+
+ return labels
+ except Exception as e:
+ logger.error(f"Error getting popular labels: {str(e)}")
+ return []
+
+ async def search_labels(self, query: str, limit: int = 10) -> list[str]:
+ """Search for node labels that match the given query"""
+ label = self._get_label()
+ try:
+ search_query = f"""
+ MATCH (n:{label})
+ WHERE LOWER(n.entity_id) CONTAINS LOWER($query)
+ OR LOWER(n.description) CONTAINS LOWER($query)
+ OR LOWER(n.keywords) CONTAINS LOWER($query)
+ RETURN DISTINCT n.entity_id
+ ORDER BY n.entity_id
+ LIMIT $limit
+ """
+ result = self.get_all(
+ self.connection.execute(search_query, {"query": query, "limit": limit})
+ )
+
+ labels = []
+ for query_result in result:
+ if not query_result.has_next():
+ continue
+ while query_result.has_next():
+ row = query_result.get_next()
+ labels.append(row[0])
+
+ return labels
+ except Exception as e:
+ logger.error(f"Error searching labels: {str(e)}")
+ return []
diff --git a/lightrag/kg/networkx_impl.py b/lightrag/kg/networkx_impl.py
index 1f716ba014..37d0365681 100644
--- a/lightrag/kg/networkx_impl.py
+++ b/lightrag/kg/networkx_impl.py
@@ -24,6 +24,16 @@
@final
@dataclass
class NetworkXStorage(BaseGraphStorage):
+ def __init__(self, namespace, global_config, embedding_func, workspace=None):
+ super().__init__(
+ namespace=namespace,
+ workspace=workspace or "base",
+ global_config=global_config,
+ embedding_func=embedding_func,
+ )
+ # Initialize attributes that would normally be set by __post_init__
+ self.__post_init__()
+
@staticmethod
def load_nx_graph(file_name) -> nx.Graph:
if os.path.exists(file_name):
diff --git a/lightrag/lightrag.py b/lightrag/lightrag.py
index e2a8209778..055a65099b 100644
--- a/lightrag/lightrag.py
+++ b/lightrag/lightrag.py
@@ -20,6 +20,7 @@
Optional,
List,
Dict,
+ TypedDict,
)
from lightrag.constants import (
DEFAULT_MAX_GLEANING,
@@ -85,7 +86,6 @@
from .utils import (
Tokenizer,
TiktokenTokenizer,
- EmbeddingFunc,
always_get_an_event_loop,
compute_mdhash_id,
lazy_external_import,
@@ -109,6 +109,46 @@
config.read("config.ini", "utf-8")
+class EntityData(TypedDict):
+ entity_id: str
+ entity_type: str
+ description: str
+ source_id: str
+ file_path: str
+ created_at: int
+ entity_name: str
+
+
+class RelationshipData(TypedDict):
+ src_id: str
+ tgt_id: str
+ source_id: str
+ file_path: str
+ created_at: int
+ content: str
+
+
+class ChunkData(TypedDict):
+ full_doc_id: str
+ content: str
+ tokens: int
+ chunk_order_index: int
+ file_path: str
+
+
+# Define the storage namespace for LightRAG
+# class NameSpace(StorageNameSpace):
+# KV_STORE_LLM_RESPONSE_CACHE = "lightrag_llm_response_cache"
+# KV_STORE_FULL_DOCS = "lightrag_full_docs"
+# KV_STORE_TEXT_CHUNKS = "lightrag_text_chunks"
+# GRAPH_STORE_CHUNK_ENTITY_RELATION = "lightrag_chunk_entity_relation_graph"
+# VECTOR_STORE_ENTITIES = "lightrag_entities_vdb"
+# VECTOR_STORE_RELATIONSHIPS = "lightrag_relationships_vdb"
+# VECTOR_STORE_CHUNKS = "lightrag_chunks_vdb"
+# DOC_STATUS = "lightrag_doc_status" # Storage for document processing statuses
+# CHUNK_DATA = "lightrag_chunk_data" # Storage for chunk metadata
+
+
@final
@dataclass
class LightRAG:
@@ -212,7 +252,7 @@ class LightRAG:
)
"""Number of overlapping tokens between consecutive text chunks to preserve context."""
- tokenizer: Optional[Tokenizer] = field(default=None)
+ tokenizer: Tokenizer = field(default=TiktokenTokenizer())
"""
A function that returns a Tokenizer instance.
If None, and a `tiktoken_model_name` is provided, a TiktokenTokenizer will be created.
@@ -255,7 +295,7 @@ class LightRAG:
# Embedding
# ---
- embedding_func: EmbeddingFunc | None = field(default=None)
+ embedding_func: Any = field(default=None)
"""Function for computing text embeddings. Must be set before use."""
embedding_batch_num: int = field(default=int(os.getenv("EMBEDDING_BATCH_NUM", 10)))
@@ -472,6 +512,13 @@ def __post_init__(self):
queue_name="Embedding func",
)(self.embedding_func)
+ # d8 /
+ # d88~\ _d88__ e88~-_ 888-~\ /~~~8e e88~88e e88~~8e
+ # C888 888 d888 i 888 88b 888 888 d888 88b
+ # Y88b 888 8888 | 888 e88~-888 "88_88" 8888__888
+ # 888D 888 Y888 ' 888 C888 888 / Y888 ,
+ # \_88P "88_/ "88_-~ 888 "88_-888 Cb "88___/
+ # Y8""8D
# Initialize all storages
self.key_string_value_json_storage_cls: type[BaseKVStorage] = (
self._get_storage_class(self.kv_storage)
@@ -813,7 +860,7 @@ async def get_knowledge_graph(
self,
node_label: str,
max_depth: int = 3,
- max_nodes: int = None,
+ max_nodes: int = 0,
) -> KnowledgeGraph:
"""Get knowledge graph for a given label
@@ -936,7 +983,7 @@ def insert_custom_chunks(
self,
full_text: str,
text_chunks: list[str],
- doc_id: str | list[str] | None = None,
+ doc_id: str | None = None,
) -> None:
loop = always_get_an_event_loop()
loop.run_until_complete(
@@ -1006,7 +1053,7 @@ async def ainsert_custom_chunks(
async def apipeline_enqueue_documents(
self,
input: str | list[str],
- ids: list[str] | None = None,
+ ids: str | list[str] | None = None,
file_paths: str | list[str] | None = None,
track_id: str | None = None,
) -> str:
@@ -1471,7 +1518,7 @@ async def process_document(
split_by_character: str | None,
split_by_character_only: bool,
pipeline_status: dict,
- pipeline_status_lock: asyncio.Lock,
+ pipeline_status_lock,
semaphore: asyncio.Semaphore,
) -> None:
"""Process single document"""
@@ -1647,8 +1694,11 @@ async def process_document(
}
)
- # Concurrency is controlled by keyed lock for individual entities and relationships
- if file_extraction_stage_ok:
+ # Concurrency is controlled by graph db lock for individual entities and relationships
+ if (
+ file_extraction_stage_ok
+ and entity_relation_task is not None
+ ):
try:
# Get chunk_results from entity_relation_task
chunk_results = await entity_relation_task
@@ -1808,7 +1858,7 @@ async def _process_extract_entities(
chunk_results = await extract_entities(
chunk,
global_config=asdict(self),
- pipeline_status=pipeline_status,
+ pipeline_status=pipeline_status or {},
pipeline_status_lock=pipeline_status_lock,
llm_response_cache=self.llm_response_cache,
text_chunks_storage=self.text_chunks,
@@ -1817,9 +1867,10 @@ async def _process_extract_entities(
except Exception as e:
error_msg = f"Failed to extract entities and relationships: {str(e)}"
logger.error(error_msg)
- async with pipeline_status_lock:
- pipeline_status["latest_message"] = error_msg
- pipeline_status["history_messages"].append(error_msg)
+ if pipeline_status_lock and pipeline_status:
+ async with pipeline_status_lock:
+ pipeline_status["latest_message"] = error_msg
+ pipeline_status["history_messages"].append(error_msg)
raise e
async def _insert_done(
@@ -1846,13 +1897,13 @@ async def _insert_done(
log_message = "In memory DB persist to disk"
logger.info(log_message)
- if pipeline_status is not None and pipeline_status_lock is not None:
+ if pipeline_status and pipeline_status_lock:
async with pipeline_status_lock:
pipeline_status["latest_message"] = log_message
pipeline_status["history_messages"].append(log_message)
def insert_custom_kg(
- self, custom_kg: dict[str, Any], full_doc_id: str = None
+ self, custom_kg: dict[str, Any], full_doc_id: str = ""
) -> None:
loop = always_get_an_event_loop()
loop.run_until_complete(self.ainsert_custom_kg(custom_kg, full_doc_id))
@@ -1860,7 +1911,7 @@ def insert_custom_kg(
async def ainsert_custom_kg(
self,
custom_kg: dict[str, Any],
- full_doc_id: str = None,
+ full_doc_id: str = "",
) -> None:
update_storage = False
try:
@@ -1884,9 +1935,7 @@ async def ainsert_custom_kg(
"source_id": source_id,
"tokens": tokens,
"chunk_order_index": chunk_order_index,
- "full_doc_id": full_doc_id
- if full_doc_id is not None
- else source_id,
+ "full_doc_id": (full_doc_id if full_doc_id else source_id),
"file_path": file_path,
"status": DocStatus.PROCESSED,
}
@@ -1901,7 +1950,8 @@ async def ainsert_custom_kg(
)
# Insert entities into knowledge graph
- all_entities_data: list[dict[str, str]] = []
+
+ all_entities_data: list[EntityData] = []
for entity_data in custom_kg.get("entities", []):
entity_name = entity_data["entity_name"]
entity_type = entity_data.get("entity_type", "UNKNOWN")
@@ -1917,7 +1967,7 @@ async def ainsert_custom_kg(
)
# Prepare node data
- node_data: dict[str, str] = {
+ node_data: dict[str, str | int | float] = {
"entity_id": entity_name,
"entity_type": entity_type,
"description": description,
@@ -1934,7 +1984,7 @@ async def ainsert_custom_kg(
update_storage = True
# Insert relationships into knowledge graph
- all_relationships_data: list[dict[str, str]] = []
+ all_relationships_data: list[dict[str, str | int | float]] = []
for relationship_data in custom_kg.get("relationships", []):
src_id = relationship_data["src_id"]
tgt_id = relationship_data["tgt_id"]
@@ -1982,7 +2032,7 @@ async def ainsert_custom_kg(
},
)
- edge_data: dict[str, str] = {
+ edge_data: dict[str, str | int | float] = {
"src_id": src_id,
"tgt_id": tgt_id,
"description": description,
diff --git a/pyproject.toml b/pyproject.toml
index e850ce2c09..9946c6087d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -25,9 +25,11 @@ dependencies = [
"configparser",
"dotenv",
"future",
- "json_repair",
+ "json-repair",
"nano-vectordb",
"networkx",
+ "kuzu>=0.10.1",
+ "networkx>=3.4.2",
"numpy",
"pandas>=2.0.0",
"pipmaster",
@@ -38,6 +40,7 @@ dependencies = [
"tenacity",
"tiktoken",
"xlsxwriter>=3.1.0",
+ "httpx>=0.28.1",
]
[project.optional-dependencies]
@@ -47,7 +50,6 @@ api = [
"configparser",
"dotenv",
"future",
- "json_repair",
"nano-vectordb",
"networkx",
"numpy",
@@ -103,3 +105,18 @@ lightrag = ["api/webui/**/*"]
[tool.ruff]
target-version = "py310"
+
+[dependency-groups]
+dev = [
+ "pre-commit>=4.2.0",
+ "pytest>=8.4.1",
+ "pytest-asyncio>=1.0.0",
+]
+
+[tool.pytest.ini_options]
+asyncio_mode = "auto"
+addopts = "-v --tb=short"
+testpaths = ["tests"]
+python_files = ["test_*.py"]
+python_classes = ["Test*"]
+python_functions = ["test_*"]
diff --git a/run_tests.py b/run_tests.py
new file mode 100755
index 0000000000..35c9574e03
--- /dev/null
+++ b/run_tests.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python3
+"""
+LightRAG Test Suite - Quick launcher
+"""
+
+import sys
+import os
+
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+
+from tests.test_cli import main
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 0000000000..1aae5780a0
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,316 @@
+# LightRAG Test Suite
+
+This directory contains comprehensive tests for the LightRAG graph storage implementations, including both modular test structures with bilingual support and legacy support for direct execution.
+
+## Test Structure
+
+```
+tests/
+├── README.md # This file
+├── __init__.py # Package initialization
+├── conftest.py # Pytest configuration
+├── test_cli.py # Interactive CLI test runner
+├── test_graph_storage.py # Graph storage test suite
+├── test_kuzu_impl.py # KuzuDB implementation tests
+├── test_kuzu_integration.py # KuzuDB integration tests
+├── test_lightrag_ollama_chat.py # Ollama chat integration tests
+└── graph/ # Modular test suite
+ ├── __init__.py # Package initialization
+ ├── main.py # Direct execution entry point
+ ├── core/ # Core utilities
+ │ ├── __init__.py # Package initialization
+ │ ├── storage_setup.py # Storage initialization
+ │ └── translation_engine.py # Translation system
+ ├── tests/ # Individual test modules
+ │ ├── __init__.py # Package initialization
+ │ ├── basic.py # Basic operations
+ │ ├── advanced.py # Advanced operations
+ │ ├── batch.py # Batch operations
+ │ ├── special_chars.py # Special character handling
+ │ └── undirected.py # Undirected graph properties
+ └── translations/ # Translation files
+ ├── __init__.py # Package initialization
+ ├── common.py # Common translations
+ ├── utility.py # Utility translations
+ ├── basic_test.py # Basic test translations
+ ├── advanced_test.py # Advanced test translations
+ ├── batch_test.py # Batch test translations
+ ├── special_char_test.py # Special character test translations
+ └── undirected_test.py # Undirected test translations
+```
+
+## Prerequisites
+
+1. **Python Environment**: Ensure you have Python 3.11+ installed
+2. **Dependencies**: Install required packages using:
+
+ ```bash
+ pip install -r requirements.txt
+ # OR using uv
+ uv sync
+ ```
+
+3. **Environment Configuration** (Optional):
+
+ ```bash
+ # Copy example environment file
+ cp .env.example .env
+
+ # Edit .env to configure storage backends
+ # Default: NetworkXStorage (no additional setup required)
+ # For KuzuDB: LIGHTRAG_GRAPH_STORAGE=KuzuDBStorage
+ ```
+
+## Quick Access 🚀
+
+Use the root-level launcher for the easiest testing experience:
+
+```bash
+# Interactive mode with bilingual language selection
+ run_tests.py
+
+# Or make it executable and run directly
+chmod +x run_tests.py && ./run_tests.py
+```
+
+## Running Tests
+
+### 1. Quick Start - All Tests
+
+```bash
+# Run all tests with pytest
+ tests/ -v
+```
+
+### 2. Interactive CLI Test Runner 🎯 [RECOMMENDED]
+
+**The easiest way to run tests** - Interactive CLI with bilingual support:
+
+```bash
+# Interactive mode (starts with bilingual language selection)
+ tests/test_cli.py
+
+# Or use the root-level launcher
+ run_tests.py
+```
+
+**Quick CLI Mode** - Skip interaction by providing all parameters:
+
+```bash
+# Run specific test with English interface
+ tests/test_cli.py --language english --storage NetworkXStorage --tests basic
+
+# Run multiple tests
+ tests/test_cli.py --language english --storage NetworkXStorage --tests basic advanced
+
+# Run all tests with Chinese interface
+ tests/test_cli.py --language chinese --storage KuzuDBStorage --tests all
+```
+
+**CLI Features:**
+
+- 🌐 **Bilingual Support**: Always starts with bilingual language selection
+- 🔧 **Storage Backend Selection**: NetworkX, KuzuDB, Neo4j, or MongoDB
+- 🎯 **Test Selection**: Choose specific tests or run all
+- 📊 **Rich Output**: Colored output with progress indicators and summaries
+- ⚡ **Quick Mode**: Non-interactive execution with command-line parameters
+- 🔄 **Repeatable**: Option to run additional tests after completion
+
+### 3. Pytest Integration
+
+For traditional pytest workflows:
+
+```bash
+# Run all graph storage tests
+ tests/test_graph_storage.py -v
+
+# Run specific test categories
+ tests/test_graph_storage.py::test_basic_graph_operations -v
+ tests/test_graph_storage.py::test_advanced_graph_operations -v
+ tests/test_graph_storage.py::test_batch_graph_operations -v
+ tests/test_graph_storage.py::test_special_characters_handling -v
+ tests/test_graph_storage.py::test_undirected_graph_properties -v
+
+# Run Chinese language variants
+ tests/test_graph_storage.py::test_basic_graph_operations_chinese -v
+ tests/test_graph_storage.py::test_special_characters_handling_chinese -v
+```
+
+### 4. Storage Backend Testing
+
+```bash
+# NetworkX Storage (default, no additional setup required)
+ tests/test_graph_storage.py -v
+
+# KuzuDB Storage
+LIGHTRAG_GRAPH_STORAGE=KuzuDBStorage tests/test_graph_storage.py -v
+
+# KuzuDB specific tests
+ tests/test_kuzu_impl.py -v
+ tests/test_kuzu_integration.py -v
+
+# Neo4j Storage (requires Neo4j instance)
+LIGHTRAG_GRAPH_STORAGE=Neo4JStorage tests/test_graph_storage.py -v
+
+# MongoDB Storage (requires MongoDB instance)
+LIGHTRAG_GRAPH_STORAGE=MongoGraphStorage tests/test_graph_storage.py -v
+```
+
+### 5. Language Testing
+
+```bash
+# Test with English translations
+TEST_LANGUAGE=english tests/test_graph_storage.py -v
+
+# Test with Chinese translations
+TEST_LANGUAGE=chinese tests/test_graph_storage.py -v
+```
+
+### 6. Advanced Testing Options
+
+```bash
+# Run with detailed output
+ tests/test_graph_storage.py -v -s
+
+# Run specific test pattern
+ tests/test_graph_storage.py -k "basic" -v
+
+# Run with coverage
+ tests/test_graph_storage.py --cov=lightrag --cov-report=html
+
+# Run with parallel execution
+ tests/test_graph_storage.py -n auto
+
+# Run with specific markers
+ tests/test_graph_storage.py -m "asyncio" -v
+```
+
+## Test Categories
+
+### Basic Operations (`test_basic_graph_operations`)
+
+- Node insertion and retrieval
+- Edge creation and properties
+- Basic graph traversal
+- Property validation
+
+### Advanced Operations (`test_advanced_graph_operations`)
+
+- Complex graph structures
+- Multi-hop relationships
+- Advanced queries
+- Performance validation
+
+### Batch Operations (`test_batch_graph_operations`)
+
+- Bulk node insertion
+- Bulk edge creation
+- Transaction handling
+- Performance optimization
+
+### Special Characters (`test_special_characters_handling`)
+
+- Unicode support
+- Special character encoding
+- Internationalization
+- Edge cases
+
+### Undirected Properties (`test_undirected_graph_properties`)
+
+- Bidirectional relationships
+- Undirected graph behavior
+- Consistency validation
+- Property symmetry
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Storage Initialization Failed**
+
+ - Check if storage backend is properly configured
+ - For KuzuDB, ensure write permissions in test directory
+
+2. **Translation Errors**
+
+ ```bash
+ # Ensure TEST_LANGUAGE is set correctly
+ export TEST_LANGUAGE=english # or chinese
+ ```
+
+3. **Missing Dependencies**
+
+ ```bash
+ # Install missing packages
+ -r requirements.txt
+ ```
+
+4. **Permission Errors (KuzuDB)**
+
+ ```bash
+ # Ensure write permissions for temporary directories
+ chmod 755 /tmp
+ ```
+
+5. **Interactive Mode for Debugging**
+
+ ```bash
+ # Use interactive CLI for detailed debugging
+ tests/test_cli.py --language english --storage NetworkXStorage --tests basic
+ # This provides more detailed output for debugging specific issues
+ ```
+
+6. **CLI Mode Advantages**
+ - The CLI provides better error reporting and progress tracking
+ - Especially useful for beginners or when debugging storage issues
+
+### Environment Variables
+
+| Variable | Description | Default |
+| ------------------------ | --------------------------- | ----------------- |
+| `LIGHTRAG_GRAPH_STORAGE` | Storage backend to use | `NetworkXStorage` |
+| `TEST_LANGUAGE` | Language for test output | `english` |
+| `KUZU_DB_PATH` | KuzuDB database path | Auto-generated |
+| `WORKING_DIR` | Working directory for tests | `./rag_storage` |
+
+## Test Results
+
+### Expected Output
+
+Successful test runs should show:
+
+- ✅ All tests passing
+- 📊 Coverage information (if enabled)
+- 🌐 Bilingual output (if language variants are run)
+- 🔄 Proper cleanup of temporary resources
+
+### Performance Benchmarks
+
+Typical execution times:
+
+- Basic operations: ~0.1-0.2 seconds
+- Advanced operations: ~0.2-0.5 seconds
+- Batch operations: ~0.3-0.8 seconds
+- Full test suite: ~1-2 seconds
+
+## Contributing
+
+When adding new tests:
+
+1. **Follow the modular structure** - Add new test files to `tests/graph/tests/`
+2. **Add translations** - Create corresponding translation files in `translations/`
+3. **Update pytest integration** - Add new tests to `test_graph_storage.py`
+4. **Document changes** - Update this README with new test categories
+
+## Support
+
+For issues or questions:
+
+1. Check the troubleshooting section above
+2. Review test logs for specific error messages
+3. Ensure all dependencies are properly installed
+4. Verify environment configuration
+
+---
+
+_This test suite provides comprehensive coverage for LightRAG graph storage implementations with support for multiple backends and languages._
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000000..360a722b4a
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,3 @@
+"""
+Test package for LightRAG
+"""
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000000..7ed859cabe
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,157 @@
+import pytest
+import os
+import tempfile
+import importlib
+from dotenv import load_dotenv
+
+# Load environment variables
+load_dotenv(dotenv_path=".env", override=False)
+
+
+# Mock embedding function for testing
+def mock_embedding_func(text):
+ """Mock embedding function that returns a fixed vector"""
+ if isinstance(text, str):
+ # Simple hash-based embedding for consistency
+ hash_val = hash(text)
+ return [float((hash_val >> i) & 1) for i in range(384)]
+ elif isinstance(text, list):
+ return [mock_embedding_func(item) for item in text]
+ else:
+ return [0.1] * 384
+
+
+# Storage configuration mapping
+STORAGES = {
+ "NetworkXStorage": "lightrag.kg.networkx_impl",
+ "Neo4JStorage": "lightrag.kg.neo4j_impl",
+ "KuzuDBStorage": "lightrag.kg.kuzu_impl",
+ "MongoDBStorage": "lightrag.kg.mongo_impl",
+ "PGGraphStorage": "lightrag.kg.postgres_impl",
+ "MemgraphStorage": "lightrag.kg.memgraph_impl",
+}
+
+
+def setup_kuzu_environment():
+ """Set up temporary KuzuDB environment for testing"""
+ temp_dir = tempfile.mkdtemp(prefix="kuzu_test_")
+ kuzu_db_path = os.path.join(temp_dir, "test_graph.db")
+ os.environ["KUZU_DB_PATH"] = kuzu_db_path
+ return temp_dir, kuzu_db_path
+
+
+def cleanup_kuzu_environment(temp_dir):
+ """Clean up temporary KuzuDB environment"""
+ import shutil
+
+ if os.path.exists(temp_dir):
+ shutil.rmtree(temp_dir)
+
+
+def initialize_share_data():
+ """Initialize shared data for NetworkXStorage"""
+ try:
+ from lightrag.kg.shared_storage import initialize_share_data
+
+ initialize_share_data()
+ except ImportError:
+ # If shared_storage doesn't exist, skip initialization
+ pass
+
+
+async def create_storage_instance(storage_type="KuzuDBStorage"):
+ """Create a storage instance for testing"""
+
+ # KuzuDB special handling
+ temp_dir = None
+ if storage_type == "KuzuDBStorage":
+ temp_dir, kuzu_db_path = setup_kuzu_environment()
+
+ # Dynamic import of storage module
+ module_path = STORAGES.get(storage_type)
+ if not module_path:
+ raise ValueError(f"Unknown storage type: {storage_type}")
+
+ try:
+ module = importlib.import_module(module_path)
+ storage_class = getattr(module, storage_type)
+ except (ImportError, AttributeError) as e:
+ if temp_dir:
+ cleanup_kuzu_environment(temp_dir)
+ raise ImportError(f"Failed to import {storage_type}: {str(e)}")
+
+ # Initialize storage instance
+ global_config = {
+ "embedding_batch_num": 10,
+ "vector_db_storage_cls_kwargs": {"cosine_better_than_threshold": 0.5},
+ "working_dir": os.environ.get("WORKING_DIR", "./rag_storage"),
+ "max_graph_nodes": 1000,
+ }
+
+ # Initialize shared data for NetworkXStorage
+ if storage_type == "NetworkXStorage":
+ initialize_share_data()
+
+ try:
+ # KuzuDB needs special initialization parameters
+ if storage_type == "KuzuDBStorage":
+ storage = storage_class(
+ namespace="test_graph",
+ global_config=global_config,
+ embedding_func=mock_embedding_func,
+ workspace="test_workspace",
+ )
+ else:
+ storage = storage_class(
+ namespace="test_graph",
+ global_config=global_config,
+ embedding_func=mock_embedding_func,
+ )
+
+ # Initialize connection
+ await storage.initialize()
+
+ # Store temp directory info for cleanup
+ if temp_dir:
+ storage._temp_dir = temp_dir
+
+ return storage
+ except Exception as e:
+ if temp_dir:
+ cleanup_kuzu_environment(temp_dir)
+ raise RuntimeError(f"Failed to initialize {storage_type}: {str(e)}")
+
+
+@pytest.fixture
+async def storage():
+ """Create a test storage instance"""
+ # Default to KuzuDBStorage for testing
+ storage_type = os.environ.get("LIGHTRAG_GRAPH_STORAGE", "KuzuDBStorage")
+
+ storage_instance = await create_storage_instance(storage_type)
+
+ yield storage_instance
+
+ # Cleanup
+ try:
+ if hasattr(storage_instance, "_temp_dir"):
+ cleanup_kuzu_environment(storage_instance._temp_dir)
+ await storage_instance.finalize()
+ except Exception:
+ pass # Ignore cleanup errors
+
+
+@pytest.fixture
+async def kuzu_storage():
+ """Create a KuzuDB storage instance for testing"""
+ storage_instance = await create_storage_instance("KuzuDBStorage")
+
+ yield storage_instance
+
+ # Cleanup
+ try:
+ if hasattr(storage_instance, "_temp_dir"):
+ cleanup_kuzu_environment(storage_instance._temp_dir)
+ await storage_instance.finalize()
+ except Exception:
+ pass # Ignore cleanup errors
diff --git a/tests/graph/__init__.py b/tests/graph/__init__.py
new file mode 100644
index 0000000000..73490cabe6
--- /dev/null
+++ b/tests/graph/__init__.py
@@ -0,0 +1,7 @@
+"""
+Test Graph Storage Package
+Organized test suite for graph storage implementations
+"""
+
+__version__ = "1.0.0"
+__author__ = "LightRAG Team"
diff --git a/tests/graph/core/__init__.py b/tests/graph/core/__init__.py
new file mode 100644
index 0000000000..e2a25cac81
--- /dev/null
+++ b/tests/graph/core/__init__.py
@@ -0,0 +1,3 @@
+"""
+Core utilities for graph storage testing
+"""
diff --git a/tests/graph/core/storage_setup.py b/tests/graph/core/storage_setup.py
new file mode 100644
index 0000000000..f3056206ba
--- /dev/null
+++ b/tests/graph/core/storage_setup.py
@@ -0,0 +1,169 @@
+"""
+Storage initialization and setup utilities
+"""
+
+import os
+import sys
+import importlib
+import tempfile
+import shutil
+from typing import Tuple
+from ascii_colors import ASCIIColors
+
+# Add parent directory to path for imports
+# sys.path.append(
+# os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+# )
+
+from lightrag.kg import (
+ STORAGE_IMPLEMENTATIONS,
+ STORAGE_ENV_REQUIREMENTS,
+ STORAGES,
+ verify_storage_implementation,
+)
+from lightrag.kg.shared_storage import initialize_share_data
+from .translation_engine import t
+
+
+async def mock_embedding_func(texts):
+ """Mock embedding function for testing"""
+ import numpy as np
+
+ return np.random.rand(len(texts), 10) # Return 10-dimensional random vectors
+
+
+def test_check_env_file() -> bool:
+ """
+ Check if .env file exists, issue warning if not
+ Returns True to continue, False to exit
+ """
+ if not os.path.exists(".env"):
+ warning_msg = t("warning_no_env")
+ ASCIIColors.yellow(warning_msg)
+
+ # Check if running in interactive terminal
+ if sys.stdin.isatty():
+ response = input(t("continue_execution"))
+ if response.lower() != "yes":
+ ASCIIColors.red(t("test_cancelled"))
+ return False
+ return True
+
+
+def setup_kuzu_test_environment() -> Tuple[str, str]:
+ """
+ Setup KuzuDB test environment
+ Returns tuple of (test_dir, kuzu_db_path)
+ """
+ # Create temporary directory for KuzuDB testing
+ test_dir = tempfile.mkdtemp(prefix="kuzu_test_")
+ kuzu_db_path = os.path.join(test_dir, "test_kuzu.db")
+
+ # Set environment variables
+ os.environ["KUZU_DB_PATH"] = kuzu_db_path
+ os.environ["KUZU_WORKSPACE"] = "test_workspace"
+
+ return test_dir, kuzu_db_path
+
+
+def cleanup_kuzu_test_environment(test_dir: str) -> None:
+ """
+ Cleanup KuzuDB test environment
+ """
+ try:
+ if os.path.exists(test_dir):
+ shutil.rmtree(test_dir)
+ except Exception as e:
+ ASCIIColors.yellow(t("warning_cleanup_temp_dir_failed") % str(e))
+
+
+async def initialize_graph_test_storage():
+ """
+ Initialize graph storage instance based on environment variables
+ Returns initialized storage instance or None if failed
+ """
+ # Get graph storage type from environment
+ graph_storage_type = os.getenv("LIGHTRAG_GRAPH_STORAGE", "NetworkXStorage")
+
+ # Verify storage type is valid
+ try:
+ verify_storage_implementation("GRAPH_STORAGE", graph_storage_type)
+ except ValueError as e:
+ ASCIIColors.red(t("error_general") % str(e))
+ ASCIIColors.yellow(
+ t("supported_graph_storage_types")
+ % ", ".join(STORAGE_IMPLEMENTATIONS["GRAPH_STORAGE"]["implementations"])
+ )
+ return None
+
+ # Check required environment variables
+ required_env_vars = STORAGE_ENV_REQUIREMENTS.get(graph_storage_type, [])
+ missing_env_vars = [var for var in required_env_vars if not os.getenv(var)]
+
+ if missing_env_vars:
+ ASCIIColors.red(
+ t("error_missing_env_vars")
+ % (graph_storage_type, ", ".join(missing_env_vars))
+ )
+ return None
+
+ # Special handling for KuzuDB: automatically setup test environment
+ temp_dir = None
+ if graph_storage_type == "KuzuDBStorage":
+ temp_dir, kuzu_db_path = setup_kuzu_test_environment()
+ ASCIIColors.cyan(t("kuzu_test_environment_setup") % kuzu_db_path)
+
+ # Dynamically import the appropriate module
+ module_path = STORAGES.get(graph_storage_type)
+ if not module_path:
+ ASCIIColors.red(t("error_module_path_not_found") % graph_storage_type)
+ if temp_dir:
+ cleanup_kuzu_test_environment(temp_dir)
+ return None
+
+ try:
+ module = importlib.import_module(module_path, package="lightrag")
+ storage_class = getattr(module, graph_storage_type)
+ except (ImportError, AttributeError) as e:
+ ASCIIColors.red(t("error_import_failed") % (graph_storage_type, str(e)))
+ if temp_dir:
+ cleanup_kuzu_test_environment(temp_dir)
+ return None
+
+ # Initialize storage instance
+ global_config = {
+ "embedding_batch_num": 10, # Batch size
+ "vector_db_storage_cls_kwargs": {
+ "cosine_better_than_threshold": 0.5 # Cosine similarity threshold
+ },
+ "working_dir": os.environ.get(
+ "WORKING_DIR", "./rag_storage"
+ ), # Working directory
+ "max_graph_nodes": 1000, # Required for KuzuDB
+ }
+
+ # NetworkXStorage requires shared_storage initialization
+ if graph_storage_type == "NetworkXStorage":
+ initialize_share_data() # Use single process mode
+
+ try:
+ storage = storage_class(
+ namespace="test_graph",
+ global_config=global_config,
+ embedding_func=mock_embedding_func,
+ workspace="test_workspace",
+ )
+
+ # Initialize connection
+ await storage.initialize()
+
+ # Store temporary directory info in storage object for later cleanup
+ if temp_dir:
+ storage._temp_dir = temp_dir
+
+ return storage
+ except Exception as e:
+ ASCIIColors.red(t("error_initialization_failed") % (graph_storage_type, str(e)))
+ if temp_dir:
+ cleanup_kuzu_test_environment(temp_dir)
+ return None
diff --git a/tests/graph/core/translation_engine.py b/tests/graph/core/translation_engine.py
new file mode 100644
index 0000000000..3b9b4c1098
--- /dev/null
+++ b/tests/graph/core/translation_engine.py
@@ -0,0 +1,95 @@
+"""
+Enhanced translation engine for multilingual test support
+"""
+
+import inspect
+import os
+from typing import Dict, Any, Optional
+
+
+# Language configuration
+def get_language():
+ """Get current language setting"""
+ return os.getenv("TEST_LANGUAGE", "chinese").lower()
+
+
+LANGUAGE = get_language()
+if LANGUAGE not in ["english", "chinese"]:
+ LANGUAGE = "chinese"
+
+language_to_index = {
+ "chinese": 0,
+ "english": 1,
+}
+
+
+def t_enhanced(key: str, translations_dict: Optional[Dict[str, Any]] = None) -> str:
+ """
+ Enhanced translation function that automatically detects the calling test function
+ by walking up the call stack until it finds a function with 'test' in its name.
+
+ Args:
+ key: Translation key to look up
+ translations_dict: Optional translations dictionary (will be imported if not provided)
+
+ Returns:
+ Translated string for the current language
+ """
+ if translations_dict is None:
+ # Import here to avoid circular imports
+ try:
+ from ..translations import get_all_translations
+
+ translations_dict = get_all_translations()
+ except ImportError:
+ # Fallback to empty dict if translations not available yet
+ translations_dict = {}
+
+ # Ensure we have a valid dictionary
+ if not isinstance(translations_dict, dict):
+ translations_dict = {}
+
+ # Get the calling function name using inspect
+ frame = inspect.currentframe()
+ try:
+ current_frame = frame.f_back if frame else None
+ test_function_name = None
+
+ # Walk up the call stack to find the first function with 'test' in its name
+ while current_frame:
+ function_name = current_frame.f_code.co_name
+ if "test" in function_name.lower():
+ test_function_name = function_name
+ break
+ current_frame = current_frame.f_back
+
+ # If we found a test function, look for translations in that section first
+ if test_function_name and test_function_name in translations_dict:
+ test_translations = translations_dict[test_function_name]
+ if isinstance(test_translations, dict) and key in test_translations:
+ value = test_translations[key]
+ if isinstance(value, list):
+ idx = language_to_index.get(get_language(), 0)
+ return value[idx]
+ return value
+
+ # Fall back to common section
+ common_translations = translations_dict.get("common", {})
+ if isinstance(common_translations, dict) and key in common_translations:
+ value = common_translations[key]
+ if isinstance(value, list):
+ idx = language_to_index.get(get_language(), 0)
+ return value[idx]
+ return value
+
+ # If not found anywhere, return the key as string
+ return str(key)
+
+ finally:
+ del frame
+
+
+# Legacy function for backward compatibility
+def t(key: str) -> str:
+ """Legacy translation function - redirects to t_enhanced"""
+ return t_enhanced(key)
diff --git a/tests/graph/main.py b/tests/graph/main.py
new file mode 100644
index 0000000000..25d5903c12
--- /dev/null
+++ b/tests/graph/main.py
@@ -0,0 +1,224 @@
+#!/usr/bin/env python
+"""
+Main test runner for organized graph storage tests
+"""
+
+import asyncio
+import argparse
+from ascii_colors import ASCIIColors
+
+from .core.storage_setup import initialize_graph_test_storage, test_check_env_file
+from .core.translation_engine import t
+from .tests.basic import test_graph_basic
+from .tests.advanced import test_graph_advanced
+from .tests.batch import test_graph_batch_operations
+from .tests.special_chars import test_graph_special_characters
+from .tests.undirected import test_graph_undirected_property
+
+
+async def run_basic_test():
+ """Run basic graph storage test"""
+ storage = await initialize_graph_test_storage()
+ if storage is None:
+ ASCIIColors.red(t("init_storage_failed"))
+ return False
+
+ try:
+ result = await test_graph_basic(storage)
+ return result
+ finally:
+ # Cleanup
+ if hasattr(storage, "close"):
+ await storage.close()
+ if hasattr(storage, "_temp_dir"):
+ from .core.storage_setup import cleanup_kuzu_test_environment
+
+ cleanup_kuzu_test_environment(storage._temp_dir)
+
+
+async def run_advanced_test():
+ """Run advanced graph storage test"""
+ storage = await initialize_graph_test_storage()
+ if storage is None:
+ ASCIIColors.red(t("init_storage_failed"))
+ return False
+
+ try:
+ ASCIIColors.blue(t("starting_advanced_test"))
+ result = await test_graph_advanced(storage)
+ return result
+ finally:
+ # Cleanup
+ if hasattr(storage, "close"):
+ await storage.close()
+ if hasattr(storage, "_temp_dir"):
+ from .core.storage_setup import cleanup_kuzu_test_environment
+
+ cleanup_kuzu_test_environment(storage._temp_dir)
+
+
+async def run_batch_test():
+ """Run batch operations graph storage test"""
+ storage = await initialize_graph_test_storage()
+ if storage is None:
+ ASCIIColors.red(t("init_storage_failed"))
+ return False
+
+ try:
+ ASCIIColors.blue(t("starting_batch_operations_test"))
+ result = await test_graph_batch_operations(storage)
+ return result
+ finally:
+ # Cleanup
+ if hasattr(storage, "close"):
+ await storage.close()
+ if hasattr(storage, "_temp_dir"):
+ from .core.storage_setup import cleanup_kuzu_test_environment
+
+ cleanup_kuzu_test_environment(storage._temp_dir)
+
+
+async def run_special_characters_test():
+ """Run special characters graph storage test"""
+ storage = await initialize_graph_test_storage()
+ if storage is None:
+ ASCIIColors.red(t("init_storage_failed"))
+ return False
+
+ try:
+ ASCIIColors.blue(t("starting_special_character_test"))
+ result = await test_graph_special_characters(storage)
+ return result
+ finally:
+ # Cleanup
+ if hasattr(storage, "close"):
+ await storage.close()
+ if hasattr(storage, "_temp_dir"):
+ from .core.storage_setup import cleanup_kuzu_test_environment
+
+ cleanup_kuzu_test_environment(storage._temp_dir)
+
+
+async def run_undirected_test():
+ """Run undirected graph property test"""
+ storage = await initialize_graph_test_storage()
+ if storage is None:
+ ASCIIColors.red(t("init_storage_failed"))
+ return False
+
+ try:
+ ASCIIColors.blue(t("starting_undirected_graph_test"))
+ result = await test_graph_undirected_property(storage)
+ return result
+ finally:
+ # Cleanup
+ if hasattr(storage, "close"):
+ await storage.close()
+ if hasattr(storage, "_temp_dir"):
+ from .core.storage_setup import cleanup_kuzu_test_environment
+
+ cleanup_kuzu_test_environment(storage._temp_dir)
+
+
+async def main():
+ """Main test runner"""
+ parser = argparse.ArgumentParser(description="Graph Storage Test Suite")
+ parser.add_argument(
+ "--test",
+ choices=["basic", "advanced", "batch", "special", "undirected", "all"],
+ default="basic",
+ help="Test type to run",
+ )
+ parser.add_argument(
+ "--language",
+ choices=["chinese", "english"],
+ default="chinese",
+ help="Language for test output",
+ )
+
+ args = parser.parse_args()
+
+ # Set language
+ import os
+
+ os.environ["TEST_LANGUAGE"] = args.language
+
+ # Check environment
+ if not test_check_env_file():
+ return
+
+ # Print header
+ print(t("program_title"))
+
+ # Run selected test
+ if args.test == "basic":
+ success = await run_basic_test()
+ if success:
+ ASCIIColors.green("✅ Basic test passed!")
+ else:
+ ASCIIColors.red("❌ Basic test failed!")
+ elif args.test == "advanced":
+ success = await run_advanced_test()
+ if success:
+ ASCIIColors.green("✅ Advanced test passed!")
+ else:
+ ASCIIColors.red("❌ Advanced test failed!")
+ elif args.test == "batch":
+ success = await run_batch_test()
+ if success:
+ ASCIIColors.green("✅ Batch operations test passed!")
+ else:
+ ASCIIColors.red("❌ Batch operations test failed!")
+ elif args.test == "special":
+ success = await run_special_characters_test()
+ if success:
+ ASCIIColors.green("✅ Special characters test passed!")
+ else:
+ ASCIIColors.red("❌ Special characters test failed!")
+ elif args.test == "undirected":
+ success = await run_undirected_test()
+ if success:
+ ASCIIColors.green("✅ Undirected graph property test passed!")
+ else:
+ ASCIIColors.red("❌ Undirected graph property test failed!")
+ elif args.test == "all":
+ # Run all available tests
+ tests = [
+ ("basic", run_basic_test),
+ ("advanced", run_advanced_test),
+ ("batch", run_batch_test),
+ ("special", run_special_characters_test),
+ ("undirected", run_undirected_test),
+ ]
+
+ all_passed = True
+ for test_name, test_func in tests:
+ ASCIIColors.blue(f"\n=== Running {test_name} test ===")
+ try:
+ success = await test_func()
+ if success:
+ ASCIIColors.green(f"✅ {test_name.capitalize()} test passed!")
+ else:
+ ASCIIColors.red(f"❌ {test_name.capitalize()} test failed!")
+ all_passed = False
+ except Exception as e:
+ ASCIIColors.red(
+ f"❌ {test_name.capitalize()} test failed with error: {e}"
+ )
+ all_passed = False
+
+ if all_passed:
+ ASCIIColors.green("\n🎉 All tests passed!")
+ else:
+ ASCIIColors.red("\n❌ Some tests failed!")
+ else:
+ ASCIIColors.yellow(
+ f"Test type '{args.test}' not yet implemented in organized structure"
+ )
+ ASCIIColors.cyan(
+ "Currently available: basic, advanced, batch, special, undirected, all"
+ )
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/tests/graph/tests/__init__.py b/tests/graph/tests/__init__.py
new file mode 100644
index 0000000000..606ecdd514
--- /dev/null
+++ b/tests/graph/tests/__init__.py
@@ -0,0 +1,6 @@
+"""
+Test modules for graph storage implementations
+
+This module serves as the entry point for test modules.
+All utility functions are imported from core/storage_setup.py to avoid duplication.
+"""
diff --git a/tests/graph/tests/advanced.py b/tests/graph/tests/advanced.py
new file mode 100644
index 0000000000..38dfb89fd9
--- /dev/null
+++ b/tests/graph/tests/advanced.py
@@ -0,0 +1,249 @@
+"""
+Advanced graph storage test module
+"""
+
+from ascii_colors import ASCIIColors
+from lightrag.types import KnowledgeGraph
+from ..core.translation_engine import t_enhanced as t
+
+
+async def test_graph_advanced(storage):
+ """
+ 测试图数据库的高级操作:
+ 1. 使用 node_degree 获取节点的度数
+ 2. 使用 edge_degree 获取边的度数
+ 3. 使用 get_node_edges 获取节点的所有边
+ 4. 使用 get_all_labels 获取所有标签
+ 5. 使用 get_knowledge_graph 获取知识图谱
+ 6. 使用 delete_node 删除节点
+ 7. 使用 remove_nodes 批量删除节点
+ 8. 使用 remove_edges 删除边
+ 9. 使用 drop 清理数据
+ """
+ try:
+ # 1. 插入测试数据
+ # 插入节点1: 人工智能
+ node1_id = t("artificial_intelligence")
+ node1_data = {
+ "entity_id": node1_id,
+ "description": t("ai_desc"),
+ "keywords": t("ai_keywords"),
+ "entity_type": t("tech_field"),
+ }
+ print(f"{t('insert_node')} 1: {node1_id}")
+ await storage.upsert_node(node1_id, node1_data)
+
+ # Insert node 2: Machine Learning / 插入节点2: 机器学习
+ node2_id = t("machine_learning")
+ node2_data = {
+ "entity_id": node2_id,
+ "description": t("ml_desc"),
+ "keywords": t("ml_keywords"),
+ "entity_type": t("tech_field"),
+ }
+ print(f"{t('insert_node')} 2: {node2_id}")
+ await storage.upsert_node(node2_id, node2_data)
+
+ # Insert node 3: Deep Learning / 插入节点3: 深度学习
+ node3_id = t("deep_learning")
+ node3_data = {
+ "entity_id": node3_id,
+ "description": t("dl_desc"),
+ "keywords": t("dl_keywords"),
+ "entity_type": t("tech_field"),
+ }
+ print(f"{t('insert_node')} 3: {node3_id}")
+ await storage.upsert_node(node3_id, node3_data)
+
+ # Insert edge 1: AI -> ML / 插入边1: 人工智能 -> 机器学习
+ edge1_data = {
+ "relationship": t("contains"),
+ "weight": 1.0,
+ "description": t("ai_contains_ml"),
+ }
+ print(f"{t('insert_edge')} 1: {node1_id} -> {node2_id}")
+ await storage.upsert_edge(node1_id, node2_id, edge1_data)
+
+ # Insert edge 2: ML -> DL / 插入边2: 机器学习 -> 深度学习
+ edge2_data = {
+ "relationship": t("contains"),
+ "weight": 1.0,
+ "description": t("ml_contains_dl"),
+ }
+ print(f"{t('insert_edge')} 2: {node2_id} -> {node3_id}")
+ await storage.upsert_edge(node2_id, node3_id, edge2_data)
+
+ # 2. Test node_degree - get node degree / 测试 node_degree - 获取节点的度数
+ print(f"== {t('test_node_degree')}: {node1_id} ==")
+ node1_degree = await storage.node_degree(node1_id)
+ print(t("node_degree_display") % (node1_id, node1_degree))
+ assert node1_degree == 1, t("node_degree_should_be") % (
+ node1_id,
+ 1,
+ node1_degree,
+ )
+
+ # 2.1 Test all node degrees / 测试所有节点的度数
+ print(f"== {t('test_all_node_degrees')}")
+ node2_degree = await storage.node_degree(node2_id)
+ node3_degree = await storage.node_degree(node3_id)
+ print(t("node_degree_display") % (node2_id, node2_degree))
+ print(t("node_degree_display") % (node3_id, node3_degree))
+ assert node2_degree == 2, t("node_degree_should_be") % (
+ node2_id,
+ 2,
+ node2_degree,
+ )
+ assert node3_degree == 1, t("node_degree_should_be") % (
+ node3_id,
+ 1,
+ node3_degree,
+ )
+
+ # 3. Test edge_degree - get edge degree / 测试 edge_degree - 获取边的度数
+ print(f"== {t('test_edge_degree')}: {node1_id} -> {node2_id} ==")
+ edge_degree = await storage.edge_degree(node1_id, node2_id)
+ print(t("edge_degree_display") % (node1_id, node2_id, edge_degree))
+ assert edge_degree == 3, t("edge_degree_should_be") % (
+ node1_id,
+ node2_id,
+ 3,
+ edge_degree,
+ )
+
+ # 3.1 Test reverse edge degree - verify undirected graph property / 测试反向边的度数 - 验证无向图特性
+ print(f"== {t('test_reverse_edge_degree')}: {node2_id} -> {node1_id}")
+ reverse_edge_degree = await storage.edge_degree(node2_id, node1_id)
+ print(t("edge_degree_display") % (node2_id, node1_id, reverse_edge_degree))
+ assert edge_degree == reverse_edge_degree, t(
+ "forward_reverse_edge_inconsistent"
+ )
+ print(t("undirected_verification_success"))
+
+ # 4. Test get_node_edges - get all edges of node / 测试 get_node_edges - 获取节点的所有边
+ print(f"== {t('test_get_node_edges')}: {node2_id} ==")
+ node2_edges = await storage.get_node_edges(node2_id)
+ print(f"{t('node_degree')} {node2_id} {t('all_edges')}: {node2_edges}")
+
+ assert len(node2_edges) == 2, t("node_should_have_edges") % (
+ node2_id,
+ 2,
+ len(node2_edges),
+ )
+
+ # 4.1 Verify undirected graph property of node edges / 验证节点边的无向图特性
+ print(f"== {t('verify_node_edges_undirected')}")
+ # Check if contains connections with node1 and node3 (regardless of direction) / 检查是否包含与node1和node3的连接关系(无论方向)
+ has_connection_with_node1 = False
+ has_connection_with_node3 = False
+ for edge in node2_edges:
+ # Check if has connection with node1 (regardless of direction) / 检查是否有与node1的连接(无论方向)
+ if (edge[0] == node1_id and edge[1] == node2_id) or (
+ edge[0] == node2_id and edge[1] == node1_id
+ ):
+ has_connection_with_node1 = True
+ # Check if has connection with node3 (regardless of direction) / 检查是否有与node3的连接(无论方向)
+ if (edge[0] == node2_id and edge[1] == node3_id) or (
+ edge[0] == node3_id and edge[1] == node2_id
+ ):
+ has_connection_with_node3 = True
+
+ assert has_connection_with_node1, t("node_edge_should_contain_connection") % (
+ node2_id,
+ node1_id,
+ )
+ assert has_connection_with_node3, t("node_edge_should_contain_connection") % (
+ node2_id,
+ node3_id,
+ )
+ print(t("undirected_node_edges_success") % node2_id)
+
+ # 5. Test get_all_labels - get all labels / 测试 get_all_labels - 获取所有标签
+ print(t("test_get_all_labels"))
+ all_labels = await storage.get_all_labels()
+ print(f"{t('all_labels')}: {all_labels}")
+ assert len(all_labels) == 3, t("should_have_labels") % (3, len(all_labels))
+ assert node1_id in all_labels, t("should_be_in_label_list") % node1_id
+ assert node2_id in all_labels, t("should_be_in_label_list") % node2_id
+ assert node3_id in all_labels, t("should_be_in_label_list") % node3_id
+
+ # 6. Test get_knowledge_graph - get knowledge graph / 测试 get_knowledge_graph - 获取知识图谱
+ print(t("test_get_knowledge_graph"))
+ kg = await storage.get_knowledge_graph("*", max_depth=2, max_nodes=10)
+ print(f"{t('knowledge_graph_nodes')}: {len(kg.nodes)}")
+ print(f"{t('knowledge_graph_edges')}: {len(kg.edges)}")
+ assert isinstance(kg, KnowledgeGraph), t("result_should_be_kg_type")
+ assert len(kg.nodes) == 3, t("kg_should_have_nodes") % (3, len(kg.nodes))
+ assert len(kg.edges) == 2, t("kg_should_have_edges") % (2, len(kg.edges))
+
+ # 7. Test delete_node - delete node / 测试 delete_node - 删除节点
+ print(f"== {t('test_delete_node')}: {node3_id} == ")
+ await storage.delete_node(node3_id)
+ node3_props = await storage.get_node(node3_id)
+ print(t("query_after_deletion_display") % (node3_id, node3_props))
+ assert node3_props is None, t("node_should_be_deleted") % node3_id
+
+ # Re-insert node3 for subsequent testing / 重新插入节点3用于后续测试
+ await storage.upsert_node(node3_id, node3_data)
+ await storage.upsert_edge(node2_id, node3_id, edge2_data)
+
+ # 8. Test remove_edges - delete edges / 测试 remove_edges - 删除边
+ print(f"== {t('test_remove_edges')}: {node2_id} -> {node3_id} == ")
+ await storage.remove_edges([(node2_id, node3_id)])
+ edge_props = await storage.get_edge(node2_id, node3_id)
+ print(t("query_edge_after_deletion_display") % (node2_id, node3_id, edge_props))
+ assert edge_props is None, t("edge_should_be_deleted") % (node2_id, node3_id)
+
+ # 8.1 Verify undirected graph property for edge deletion / 验证删除边的无向图特性
+ print(f"== {t('verify_undirected_property')}: {node3_id} -> {node2_id} == ")
+ reverse_edge_props = await storage.get_edge(node3_id, node2_id)
+ print(
+ t("query_reverse_edge_after_deletion")
+ % (node3_id, node2_id, reverse_edge_props)
+ )
+ assert reverse_edge_props is None, t("reverse_edge_should_be_deleted") % (
+ node3_id,
+ node2_id,
+ )
+ print(t("undirected_deletion_success"))
+
+ # 9. Test remove_nodes - batch delete nodes / 测试 remove_nodes - 批量删除节点
+ print(f"== {t('test_remove_nodes')}: [{node2_id}, {node3_id}] == ")
+ await storage.remove_nodes([node2_id, node3_id])
+ node2_props = await storage.get_node(node2_id)
+ node3_props = await storage.get_node(node3_id)
+ print(t("query_after_deletion_display") % (node2_id, node2_props))
+ print(t("query_after_deletion_display") % (node3_id, node3_props))
+ assert node2_props is None, t("node_should_be_deleted") % node2_id
+ assert node3_props is None, t("node_should_be_deleted") % node3_id
+
+ ASCIIColors.green(t("advanced_test_complete"))
+ return True
+
+ except Exception as e:
+ ASCIIColors.red(f"{t('test_error')}: {str(e)}")
+ return False
+
+
+async def run_advanced_test():
+ """Run the advanced test standalone"""
+ from ..core.storage_setup import initialize_graph_test_storage
+
+ storage = await initialize_graph_test_storage()
+ if storage is None:
+ ASCIIColors.red(t("init_storage_failed"))
+ return False
+
+ try:
+ ASCIIColors.blue(t("starting_advanced_test"))
+ result = await test_graph_advanced(storage)
+ return result
+ finally:
+ if hasattr(storage, "close"):
+ await storage.close()
+
+
+if __name__ == "__main__":
+ import asyncio
+
+ asyncio.run(run_advanced_test())
diff --git a/tests/graph/tests/basic.py b/tests/graph/tests/basic.py
new file mode 100644
index 0000000000..80f7dd4202
--- /dev/null
+++ b/tests/graph/tests/basic.py
@@ -0,0 +1,129 @@
+"""
+Basic graph storage test module
+"""
+
+from ..core.translation_engine import t
+from ascii_colors import ASCIIColors
+
+
+async def test_graph_basic(storage):
+ """
+ Test basic graph operations:
+ 1. Insert nodes using upsert_node
+ 2. Insert edges using upsert_edge
+ 3. Read node properties using get_node
+ 4. Read edge properties using get_edge
+ """
+ try:
+ ASCIIColors.cyan(t("starting_basic_test"))
+
+ # 1. Insert first node
+ node1_id = t("artificial_intelligence")
+ node1_data = {
+ "entity_id": node1_id,
+ "description": t("ai_desc"),
+ "keywords": t("ai_keywords"),
+ "entity_type": t("tech_field"),
+ }
+ print(f"{t('insert_node')} 1: {node1_id}")
+ await storage.upsert_node(node1_id, node1_data)
+
+ # 2. Insert second node
+ node2_id = t("machine_learning")
+ node2_data = {
+ "entity_id": node2_id,
+ "description": t("ml_desc"),
+ "keywords": t("ml_keywords"),
+ "entity_type": t("tech_field"),
+ }
+ print(f"{t('insert_node')} 2: {node2_id}")
+ await storage.upsert_node(node2_id, node2_data)
+
+ # 3. Insert connecting edge
+ edge_data = {
+ "relationship": t("contains"),
+ "weight": 1.0,
+ "description": t("ai_contains_ml"),
+ }
+ print(f"{t('insert_edge')}: {node1_id} -> {node2_id}")
+ await storage.upsert_edge(node1_id, node2_id, edge_data)
+
+ # 4. Read node properties
+ print(f"{t('read_node_props')}: {node1_id}")
+ node1_props = await storage.get_node(node1_id)
+ if node1_props:
+ print(f"{t('success_read_node')}: {node1_id}")
+ print(
+ f"{t('node_desc')}: {node1_props.get('description', t('no_description'))}"
+ )
+ print(f"{t('node_type')}: {node1_props.get('entity_type', t('no_type'))}")
+ print(
+ f"{t('node_keywords')}: {node1_props.get('keywords', t('no_keywords'))}"
+ )
+ # Verify returned properties are correct
+ assert (
+ node1_props.get("entity_id") == node1_id
+ ), f"{t('node_id_mismatch')} {node1_id}, {t('actual')} {node1_props.get('entity_id')}"
+ assert node1_props.get("description") == node1_data["description"], t(
+ "node_desc_mismatch"
+ )
+ assert node1_props.get("entity_type") == node1_data["entity_type"], t(
+ "node_type_mismatch"
+ )
+ else:
+ print(f"{t('failed_read_node')}: {node1_id}")
+ assert False, f"{t('unable_read_node')}: {node1_id}"
+
+ # 5. Read edge properties
+ print(f"{t('read_edge_props')}: {node1_id} -> {node2_id}")
+ edge_props = await storage.get_edge(node1_id, node2_id)
+ if edge_props:
+ print(f"{t('success_read_edge')}: {node1_id} -> {node2_id}")
+ print(
+ f"{t('edge_relation')}: {edge_props.get('relationship', t('no_relationship'))}"
+ )
+ print(
+ f"{t('edge_desc')}: {edge_props.get('description', t('no_description'))}"
+ )
+ print(f"{t('edge_weight')}: {edge_props.get('weight', t('no_weight'))}")
+ # Verify returned properties are correct
+ assert edge_props.get("relationship") == edge_data["relationship"], t(
+ "edge_relation_mismatch"
+ )
+ assert edge_props.get("description") == edge_data["description"], t(
+ "edge_desc_mismatch"
+ )
+ assert edge_props.get("weight") == edge_data["weight"], t(
+ "edge_weight_mismatch"
+ )
+ else:
+ print(f"{t('failed_read_edge')}: {node1_id} -> {node2_id}")
+ assert False, f"{t('unable_read_edge')}: {node1_id} -> {node2_id}"
+
+ # 5.1 Verify undirected graph property - read reverse edge properties
+ print(f"{t('read_reverse_edge')}: {node2_id} -> {node1_id}")
+ reverse_edge_props = await storage.get_edge(node2_id, node1_id)
+ if reverse_edge_props:
+ print(f"{t('success_read_reverse')}: {node2_id} -> {node1_id}")
+ print(
+ f"{t('reverse_edge_relation')}: {reverse_edge_props.get('relationship', t('no_relationship'))}"
+ )
+ print(
+ f"{t('reverse_edge_desc')}: {reverse_edge_props.get('description', t('no_description'))}"
+ )
+ print(
+ f"{t('reverse_edge_weight')}: {reverse_edge_props.get('weight', t('no_weight'))}"
+ )
+ # Verify forward and reverse edge properties are the same
+ assert edge_props == reverse_edge_props, t("forward_reverse_inconsistent")
+ print(t("undirected_verification_success"))
+ else:
+ print(f"{t('failed_read_reverse_edge')}: {node2_id} -> {node1_id}")
+ assert False, f"{t('unable_read_reverse_edge')}: {node2_id} -> {node1_id}, {t('undirected_verification_failed')}"
+
+ ASCIIColors.green(t("basic_test_complete"))
+ return True
+
+ except Exception as e:
+ ASCIIColors.red(f"{t('test_error')}: {str(e)}")
+ return False
diff --git a/tests/graph/tests/batch.py b/tests/graph/tests/batch.py
new file mode 100644
index 0000000000..2b33be42fc
--- /dev/null
+++ b/tests/graph/tests/batch.py
@@ -0,0 +1,504 @@
+"""
+Batch operations test module for graph storage.
+Tests all batch operations including get_nodes_batch, node_degrees_batch,
+edge_degrees_batch, get_edges_batch, get_nodes_edges_batch, and chunk-based operations.
+"""
+
+import asyncio
+from lightrag.constants import GRAPH_FIELD_SEP
+from ..core.translation_engine import t_enhanced as t
+from ascii_colors import ASCIIColors
+
+
+async def test_graph_batch_operations(storage):
+ """
+ 测试图数据库的批量操作:
+ 1. 使用 get_nodes_batch 批量获取多个节点的属性
+ 2. 使用 node_degrees_batch 批量获取多个节点的度数
+ 3. 使用 edge_degrees_batch 批量获取多个边的度数
+ 4. 使用 get_edges_batch 批量获取多个边的属性
+ 5. 使用 get_nodes_edges_batch 批量获取多个节点的所有边
+ """
+ try:
+ chunk1_id = "1"
+ chunk2_id = "2"
+ chunk3_id = "3"
+ # 1. 插入测试数据
+ # 插入节点1: 人工智能
+ node1_id = t("artificial_intelligence")
+ node1_data = {
+ "entity_id": node1_id,
+ "description": t("ai_desc"),
+ "keywords": t("ai_keywords"),
+ "entity_type": t("tech_field"),
+ "source_id": GRAPH_FIELD_SEP.join([chunk1_id, chunk2_id]),
+ }
+ print(f"{t('insert_node_1')}: {node1_id}")
+ await storage.upsert_node(node1_id, node1_data)
+
+ # 插入节点2: 机器学习
+ node2_id = t("machine_learning")
+ node2_data = {
+ "entity_id": node2_id,
+ "description": t("ml_desc"),
+ "keywords": t("ml_keywords"),
+ "entity_type": t("tech_field"),
+ "source_id": GRAPH_FIELD_SEP.join([chunk2_id, chunk3_id]),
+ }
+ print(f"{t('insert_node_2')}: {node2_id}")
+ await storage.upsert_node(node2_id, node2_data)
+
+ # 插入节点3: 深度学习
+ node3_id = t("deep_learning")
+ node3_data = {
+ "entity_id": node3_id,
+ "description": t("dl_desc"),
+ "keywords": t("dl_keywords"),
+ "entity_type": t("tech_field"),
+ "source_id": GRAPH_FIELD_SEP.join([chunk3_id]),
+ }
+ print(f"{t('insert_node_3')}: {node3_id}")
+ await storage.upsert_node(node3_id, node3_data)
+
+ # 插入节点4: 自然语言处理
+ node4_id = t("natural_language_processing")
+ node4_data = {
+ "entity_id": node4_id,
+ "description": t("nlp_desc"),
+ "keywords": t("nlp_keywords"),
+ "entity_type": t("tech_field"),
+ }
+ print(f"{t('insert_node_4')}: {node4_id}")
+ await storage.upsert_node(node4_id, node4_data)
+
+ # 插入节点5: 计算机视觉
+ node5_id = t("computer_vision")
+ node5_data = {
+ "entity_id": node5_id,
+ "description": t("cv_desc"),
+ "keywords": t("cv_keywords"),
+ "entity_type": t("tech_field"),
+ }
+ print(f"{t('insert_node_5')}: {node5_id}")
+ await storage.upsert_node(node5_id, node5_data)
+
+ # 插入边1: 人工智能 -> 机器学习
+ edge1_data = {
+ "relationship": t("contains"),
+ "weight": 1.0,
+ "description": t("ai_contains_ml"),
+ "source_id": GRAPH_FIELD_SEP.join([chunk1_id, chunk2_id]),
+ }
+ print(f"{t('insert_edge_1')}: {node1_id} -> {node2_id}")
+ await storage.upsert_edge(node1_id, node2_id, edge1_data)
+
+ # 插入边2: 机器学习 -> 深度学习
+ edge2_data = {
+ "relationship": t("contains"),
+ "weight": 1.0,
+ "description": t("ml_contains_dl"),
+ "source_id": GRAPH_FIELD_SEP.join([chunk2_id, chunk3_id]),
+ }
+ print(f"{t('insert_edge_2')}: {node2_id} -> {node3_id}")
+ await storage.upsert_edge(node2_id, node3_id, edge2_data)
+
+ # 插入边3: 人工智能 -> 自然语言处理
+ edge3_data = {
+ "relationship": t("contains"),
+ "weight": 1.0,
+ "description": t("ai_contains_nlp"),
+ "source_id": GRAPH_FIELD_SEP.join([chunk3_id]),
+ }
+ print(f"{t('insert_edge_3')}: {node1_id} -> {node4_id}")
+ await storage.upsert_edge(node1_id, node4_id, edge3_data)
+
+ # 插入边4: 人工智能 -> 计算机视觉
+ edge4_data = {
+ "relationship": t("contains"),
+ "weight": 1.0,
+ "description": t("ai_contains_cv_desc"),
+ }
+ print(f"{t('insert_edge_4')}: {node1_id} -> {node5_id}")
+ await storage.upsert_edge(node1_id, node5_id, edge4_data)
+
+ # 插入边5: 深度学习 -> 自然语言处理
+ edge5_data = {
+ "relationship": t("dl_applied_nlp_relationship"),
+ "weight": 0.8,
+ "description": t("dl_applied_nlp_desc"),
+ }
+ print(f"{t('insert_edge_5')}: {node3_id} -> {node4_id}")
+ await storage.upsert_edge(node3_id, node4_id, edge5_data)
+
+ # 插入边6: 深度学习 -> 计算机视觉
+ edge6_data = {
+ "relationship": t("dl_applied_cv_relationship"),
+ "weight": 0.8,
+ "description": t("dl_applied_cv_desc"),
+ }
+ print(f"{t('insert_edge_6')}: {node3_id} -> {node5_id}")
+ await storage.upsert_edge(node3_id, node5_id, edge6_data)
+
+ # 2. 测试 get_nodes_batch - 批量获取多个节点的属性
+ print(t("batch_get_nodes"))
+ node_ids = [node1_id, node2_id, node3_id]
+ nodes_dict = await storage.get_nodes_batch(node_ids)
+ print(f"{t('batch_get_nodes_result')}: {nodes_dict.keys()}")
+ assert len(nodes_dict) == 3, t("should_return_nodes") % (3, len(nodes_dict))
+ assert node1_id in nodes_dict, t("should_be_in_result") % node1_id
+ assert node2_id in nodes_dict, t("should_be_in_result") % node2_id
+ assert node3_id in nodes_dict, t("should_be_in_result") % node3_id
+ assert nodes_dict[node1_id]["description"] == node1_data["description"], (
+ t("description_mismatch") % node1_id
+ )
+ assert nodes_dict[node2_id]["description"] == node2_data["description"], (
+ t("description_mismatch") % node2_id
+ )
+ assert nodes_dict[node3_id]["description"] == node3_data["description"], (
+ t("description_mismatch") % node3_id
+ )
+
+ # 3. 测试 node_degrees_batch - 批量获取多个节点的度数
+ print(t("batch_node_degrees"))
+ node_degrees = await storage.node_degrees_batch(node_ids)
+ print(f"{t('batch_node_degrees_result')}: {node_degrees}")
+ assert len(node_degrees) == 3, t("should_return_node_degrees") % (
+ 3,
+ len(node_degrees),
+ )
+ assert node1_id in node_degrees, t("should_be_in_result") % node1_id
+ assert node2_id in node_degrees, t("should_be_in_result") % node2_id
+ assert node3_id in node_degrees, t("should_be_in_result") % node3_id
+ assert node_degrees[node1_id] == 3, t("node_degree_should_be") % (
+ node1_id,
+ 3,
+ node_degrees[node1_id],
+ )
+ assert node_degrees[node2_id] == 2, t("node_degree_should_be") % (
+ node2_id,
+ 2,
+ node_degrees[node2_id],
+ )
+ assert node_degrees[node3_id] == 3, t("node_degree_should_be") % (
+ node3_id,
+ 3,
+ node_degrees[node3_id],
+ )
+
+ # 4. 测试 edge_degrees_batch - 批量获取多个边的度数
+ print(t("batch_edge_degrees"))
+ edges = [(node1_id, node2_id), (node2_id, node3_id), (node3_id, node4_id)]
+ edge_degrees = await storage.edge_degrees_batch(edges)
+ print(f"{t('batch_edge_degrees_result')}: {edge_degrees}")
+ assert len(edge_degrees) == 3, t("should_return_edge_degrees") % (
+ 3,
+ len(edge_degrees),
+ )
+ assert (
+ node1_id,
+ node2_id,
+ ) in edge_degrees, t("edge_should_be_in_result") % (node1_id, node2_id)
+ assert (
+ node2_id,
+ node3_id,
+ ) in edge_degrees, t("edge_should_be_in_result") % (node2_id, node3_id)
+ assert (
+ node3_id,
+ node4_id,
+ ) in edge_degrees, t("edge_should_be_in_result") % (node3_id, node4_id)
+
+ # 验证边的度数是否正确(源节点度数 + 目标节点度数)
+ assert (
+ edge_degrees[(node1_id, node2_id)] == 5
+ ), f"边 {node1_id} -> {node2_id} 度数应为5,实际为 {edge_degrees[(node1_id, node2_id)]}"
+ assert (
+ edge_degrees[(node2_id, node3_id)] == 5
+ ), f"边 {node2_id} -> {node3_id} 度数应为5,实际为 {edge_degrees[(node2_id, node3_id)]}"
+ assert (
+ edge_degrees[(node3_id, node4_id)] == 5
+ ), f"边 {node3_id} -> {node4_id} 度数应为5,实际为 {edge_degrees[(node3_id, node4_id)]}"
+
+ # 5. 测试 get_edges_batch - 批量获取多个边的属性
+ print(t("batch_get_edges"))
+ # 将元组列表转换为Neo4j风格的字典列表
+ edge_dicts = [{"src": src, "tgt": tgt} for src, tgt in edges]
+ edges_dict = await storage.get_edges_batch(edge_dicts)
+ print(f"{t('batch_get_edges_result')}: {edges_dict.keys()}")
+ assert len(edges_dict) == 3, t("should_return_edge_properties") % (
+ 3,
+ len(edges_dict),
+ )
+ assert (
+ node1_id,
+ node2_id,
+ ) in edges_dict, t("edge_should_be_in_result") % (node1_id, node2_id)
+ assert (
+ node2_id,
+ node3_id,
+ ) in edges_dict, t("edge_should_be_in_result") % (node2_id, node3_id)
+ assert (
+ node3_id,
+ node4_id,
+ ) in edges_dict, t("edge_should_be_in_result") % (node3_id, node4_id)
+ assert (
+ edges_dict[(node1_id, node2_id)]["relationship"]
+ == edge1_data["relationship"]
+ ), f"边 {node1_id} -> {node2_id} 关系不匹配"
+ assert (
+ edges_dict[(node2_id, node3_id)]["relationship"]
+ == edge2_data["relationship"]
+ ), f"边 {node2_id} -> {node3_id} 关系不匹配"
+ assert (
+ edges_dict[(node3_id, node4_id)]["relationship"]
+ == edge5_data["relationship"]
+ ), f"边 {node3_id} -> {node4_id} 关系不匹配"
+
+ # 5.1 测试反向边的批量获取 - 验证无向图特性
+ print(t("test_reverse_edges_batch"))
+ # 创建反向边的字典列表
+ reverse_edge_dicts = [{"src": tgt, "tgt": src} for src, tgt in edges]
+ reverse_edges_dict = await storage.get_edges_batch(reverse_edge_dicts)
+ print(f"{t('batch_get_reverse_edges_result')}: {reverse_edges_dict.keys()}")
+ assert len(reverse_edges_dict) == 3, t(
+ "should_return_reverse_edge_properties"
+ ) % (3, len(reverse_edges_dict))
+
+ # 验证正向和反向边的属性是否一致
+ for (src, tgt), props in edges_dict.items():
+ assert (
+ tgt,
+ src,
+ ) in reverse_edges_dict, t("reverse_edge_should_be_in_result") % (tgt, src)
+ assert (
+ props == reverse_edges_dict[(tgt, src)]
+ ), f"边 {src} -> {tgt} 和反向边 {tgt} -> {src} 的属性不一致"
+
+ print(t("undirected_batch_verification_success"))
+
+ # 6. 测试 get_nodes_edges_batch - 批量获取多个节点的所有边
+ print(t("test_get_nodes_edges_batch"))
+ nodes_edges = await storage.get_nodes_edges_batch([node1_id, node3_id])
+ print(f"{t('batch_get_nodes_edges_result')}: {nodes_edges.keys()}")
+ assert len(nodes_edges) == 2, t("should_return_node_edges") % (
+ 2,
+ len(nodes_edges),
+ )
+ assert node1_id in nodes_edges, t("should_be_in_result") % node1_id
+ assert node3_id in nodes_edges, t("should_be_in_result") % node3_id
+ assert len(nodes_edges[node1_id]) == 3, t("node_should_have_edges_count") % (
+ node1_id,
+ 3,
+ len(nodes_edges[node1_id]),
+ )
+ assert len(nodes_edges[node3_id]) == 3, t("node_should_have_edges_count") % (
+ node3_id,
+ 3,
+ len(nodes_edges[node3_id]),
+ )
+
+ # 6.1 验证批量获取节点边的无向图特性
+ print(t("verify_batch_nodes_edges_undirected"))
+
+ # 检查节点1的边是否包含所有相关的边(无论方向)
+ node1_outgoing_edges = [
+ (src, tgt) for src, tgt in nodes_edges[node1_id] if src == node1_id
+ ]
+ node1_incoming_edges = [
+ (src, tgt) for src, tgt in nodes_edges[node1_id] if tgt == node1_id
+ ]
+ print(
+ f"{t('node')} {node1_id} {t('node_outgoing_edges')}: {node1_outgoing_edges}"
+ )
+ print(
+ f"{t('node')} {node1_id} {t('node_incoming_edges')}: {node1_incoming_edges}"
+ )
+
+ # 检查是否包含到机器学习、自然语言处理和计算机视觉的边
+ has_edge_to_node2 = any(tgt == node2_id for _, tgt in node1_outgoing_edges)
+ has_edge_to_node4 = any(tgt == node4_id for _, tgt in node1_outgoing_edges)
+ has_edge_to_node5 = any(tgt == node5_id for _, tgt in node1_outgoing_edges)
+
+ assert has_edge_to_node2, t("node_edge_list_should_contain_edge_to") % (
+ node1_id,
+ node2_id,
+ )
+ assert has_edge_to_node4, t("node_edge_list_should_contain_edge_to") % (
+ node1_id,
+ node4_id,
+ )
+ assert has_edge_to_node5, t("node_edge_list_should_contain_edge_to") % (
+ node1_id,
+ node5_id,
+ )
+
+ # 检查节点3的边是否包含所有相关的边(无论方向)
+ node3_outgoing_edges = [
+ (src, tgt) for src, tgt in nodes_edges[node3_id] if src == node3_id
+ ]
+ node3_incoming_edges = [
+ (src, tgt) for src, tgt in nodes_edges[node3_id] if tgt == node3_id
+ ]
+ print(
+ f"{t('node')} {node3_id} {t('node_outgoing_edges')}: {node3_outgoing_edges}"
+ )
+ print(
+ f"{t('node')} {node3_id} {t('node_incoming_edges')}: {node3_incoming_edges}"
+ )
+
+ # 检查是否包含与机器学习、自然语言处理和计算机视觉的连接(忽略方向)
+ has_connection_with_node2 = any(
+ (src == node2_id and tgt == node3_id)
+ or (src == node3_id and tgt == node2_id)
+ for src, tgt in nodes_edges[node3_id]
+ )
+ has_connection_with_node4 = any(
+ (src == node3_id and tgt == node4_id)
+ or (src == node4_id and tgt == node3_id)
+ for src, tgt in nodes_edges[node3_id]
+ )
+ has_connection_with_node5 = any(
+ (src == node3_id and tgt == node5_id)
+ or (src == node5_id and tgt == node3_id)
+ for src, tgt in nodes_edges[node3_id]
+ )
+
+ assert has_connection_with_node2, t(
+ "node_edge_list_should_contain_connection"
+ ) % (node3_id, node2_id)
+ assert has_connection_with_node4, t(
+ "node_edge_list_should_contain_connection"
+ ) % (node3_id, node4_id)
+ assert has_connection_with_node5, t(
+ "node_edge_list_should_contain_connection"
+ ) % (node3_id, node5_id)
+
+ print(t("undirected_nodes_edges_verification_success"))
+
+ # 7. 测试 get_nodes_by_chunk_ids - 批量根据 chunk_ids 获取多个节点
+
+ print(t("test_get_nodes_by_chunk_ids"))
+ nodes = await storage.get_nodes_by_chunk_ids([chunk2_id])
+ assert len(nodes) == 2, t("chunk_should_have_nodes") % (
+ chunk2_id,
+ 2,
+ len(nodes),
+ )
+
+ has_node1 = any(node["entity_id"] == node1_id for node in nodes)
+ has_node2 = any(node["entity_id"] == node2_id for node in nodes)
+
+ print(t("test_single_chunk_id_multiple_nodes"))
+ assert has_node1, t("node_should_be_in_result") % node1_id
+ assert has_node2, t("node_should_be_in_result") % node2_id
+
+ print(t("test_multiple_chunk_ids_partial_match"))
+ nodes = await storage.get_nodes_by_chunk_ids([chunk2_id, chunk3_id])
+ assert len(nodes) == 3, t("chunks_should_have_nodes") % (
+ chunk2_id,
+ chunk3_id,
+ 3,
+ len(nodes),
+ )
+
+ has_node1 = any(node["entity_id"] == node1_id for node in nodes)
+ has_node2 = any(node["entity_id"] == node2_id for node in nodes)
+ has_node3 = any(node["entity_id"] == node3_id for node in nodes)
+
+ assert has_node1, t("node_should_be_in_result") % node1_id
+ assert has_node2, t("node_should_be_in_result") % node2_id
+ assert has_node3, t("node_should_be_in_result") % node3_id
+
+ # 8. 测试 get_edges_by_chunk_ids - 批量根据 chunk_ids 获取多条边
+ print(t("test_get_edges_by_chunk_ids"))
+
+ edges = await storage.get_edges_by_chunk_ids([chunk2_id])
+ assert len(edges) == 2, t("chunk_should_have_edges") % (
+ chunk2_id,
+ 2,
+ len(edges),
+ )
+ print(t("test_single_chunk_id_multiple_edges"))
+
+ has_edge_node1_node2 = any(
+ edge["source"] == node1_id and edge["target"] == node2_id for edge in edges
+ )
+ has_edge_node2_node3 = any(
+ edge["source"] == node2_id and edge["target"] == node3_id for edge in edges
+ )
+
+ assert has_edge_node1_node2, t("chunk_should_contain_edge") % (
+ chunk2_id,
+ node1_id,
+ node2_id,
+ )
+ assert has_edge_node2_node3, t("chunk_should_contain_edge") % (
+ chunk2_id,
+ node2_id,
+ node3_id,
+ )
+
+ print(t("test_multiple_chunk_ids_partial_edges"))
+ edges = await storage.get_edges_by_chunk_ids([chunk2_id, chunk3_id])
+ assert len(edges) == 3, t("chunks_should_have_edges") % (
+ chunk2_id,
+ chunk3_id,
+ 3,
+ len(edges),
+ )
+
+ has_edge_node1_node2 = any(
+ edge["source"] == node1_id and edge["target"] == node2_id for edge in edges
+ )
+ has_edge_node2_node3 = any(
+ edge["source"] == node2_id and edge["target"] == node3_id for edge in edges
+ )
+ has_edge_node1_node4 = any(
+ edge["source"] == node1_id and edge["target"] == node4_id for edge in edges
+ )
+
+ assert has_edge_node1_node2, t("chunks_should_contain_edge") % (
+ chunk2_id,
+ chunk3_id,
+ node1_id,
+ node2_id,
+ )
+ assert has_edge_node2_node3, t("chunks_should_contain_edge") % (
+ chunk2_id,
+ chunk3_id,
+ node2_id,
+ node3_id,
+ )
+ assert has_edge_node1_node4, t("chunks_should_contain_edge") % (
+ chunk2_id,
+ chunk3_id,
+ node1_id,
+ node4_id,
+ )
+
+ ASCIIColors.green(t("batch_operations_test_complete"))
+ return True
+
+ except Exception as e:
+ ASCIIColors.red(f"{t('test_error')}: {str(e)}")
+ return False
+
+
+# For direct execution/testing
+async def main():
+ """Test function for direct execution"""
+ from ..core.storage_setup import (
+ initialize_graph_test_storage,
+ )
+
+ print(t("starting_batch_operations_test"))
+ storage = await initialize_graph_test_storage()
+
+ if storage:
+ result = await test_graph_batch_operations(storage)
+ print(f"Test result: {result}")
+ else:
+ print("Storage initialization failed")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/tests/graph/tests/special_chars.py b/tests/graph/tests/special_chars.py
new file mode 100644
index 0000000000..50ecf9a98d
--- /dev/null
+++ b/tests/graph/tests/special_chars.py
@@ -0,0 +1,179 @@
+"""
+Special characters test module for graph storage.
+Tests handling of special characters in node names, descriptions, and edge properties.
+"""
+
+import asyncio
+from ..core.translation_engine import t_enhanced as t
+from ascii_colors import ASCIIColors
+
+
+async def test_graph_special_characters(storage):
+ """
+ Test graph database handling of special characters / 测试图数据库对特殊字符的处理:
+ 1. Test node names and descriptions containing single quotes, double quotes and backslashes / 测试节点名称和描述中包含单引号、双引号和反斜杠
+ 2. Test edge descriptions containing single quotes, double quotes and backslashes / 测试边的描述中包含单引号、双引号和反斜杠
+ 3. Verify special characters are correctly saved and retrieved / 验证特殊字符是否被正确保存和检索
+ """
+ try:
+ # 1. Test special characters in node names / 测试节点名称中的特殊字符
+ node1_id = t("node_with_quotes")
+ node1_data = {
+ "entity_id": node1_id,
+ "description": t("desc_with_special"),
+ "keywords": t("keywords_special"),
+ "entity_type": t("test_node"),
+ }
+ print(f"{t('insert_node_with_special_1')}: {node1_id}")
+ await storage.upsert_node(node1_id, node1_data)
+
+ # 2. Test double quotes in node names / 测试节点名称中的双引号
+ node2_id = t("node_with_double_quotes")
+ node2_data = {
+ "entity_id": node2_id,
+ "description": t("desc_with_complex"),
+ "keywords": t("keywords_json"),
+ "entity_type": t("test_node"),
+ }
+ print(f"{t('insert_node_with_special_2')}: {node2_id}")
+ await storage.upsert_node(node2_id, node2_data)
+
+ # 3. Test backslashes in node names / 测试节点名称中的反斜杠
+ node3_id = t("node_with_backslash")
+ node3_data = {
+ "entity_id": node3_id,
+ "description": t("desc_with_windows_path"),
+ "keywords": t("keywords_backslash"),
+ "entity_type": t("test_node"),
+ }
+ print(f"{t('insert_node_with_special_3')}: {node3_id}")
+ await storage.upsert_node(node3_id, node3_data)
+
+ # 4. Test special characters in edge descriptions / 测试边描述中的特殊字符
+ edge1_data = {
+ "relationship": t("special_relation"),
+ "weight": 1.0,
+ "description": t("edge_desc_special"),
+ }
+ print(f"{t('insert_edge_with_special')}: {node1_id} -> {node2_id}")
+ await storage.upsert_edge(node1_id, node2_id, edge1_data)
+
+ # 5. Test more complex special character combinations in edges / 测试边描述中的更复杂特殊字符组合
+ edge2_data = {
+ "relationship": t("complex_relation"),
+ "weight": 0.8,
+ "description": t("edge_desc_sql"),
+ }
+ print(f"{t('insert_edge_with_complex_special')}: {node2_id} -> {node3_id}")
+ await storage.upsert_edge(node2_id, node3_id, edge2_data)
+
+ # 6. Verify node special characters are correctly saved / 验证节点特殊字符是否正确保存
+ print(f"\n== {t('verify_node_special')} ==")
+ for node_id, original_data in [
+ (node1_id, node1_data),
+ (node2_id, node2_data),
+ (node3_id, node3_data),
+ ]:
+ node_props = await storage.get_node(node_id)
+ if node_props:
+ print(f"{t('read_node_success')}: {node_id}")
+ print(
+ f"{t('node_description')}: {node_props.get('description', t('no_description'))}"
+ )
+
+ # Verify node ID is correctly saved / 验证节点ID是否正确保存
+ assert node_props.get("entity_id") == node_id, t(
+ "node_id_mismatch_f"
+ ).format(node_id, node_props.get("entity_id"))
+
+ # Verify description is correctly saved / 验证描述是否正确保存
+ assert node_props.get("description") == original_data["description"], t(
+ "node_description_mismatch"
+ ).format(original_data["description"], node_props.get("description"))
+
+ print(t("node_special_char_verification_success").format(node_id))
+ else:
+ print(f"{t('read_node_props_failed')}: {node_id}")
+ assert False, t("unable_to_read_node_props").format(node_id)
+
+ # 7. Verify edge special characters are correctly saved / 验证边特殊字符是否正确保存
+ print(f"\n== {t('verify_edge_special')} ==")
+ edge1_props = await storage.get_edge(node1_id, node2_id)
+ if edge1_props:
+ print(f"{t('read_edge_success')}: {node1_id} -> {node2_id}")
+ print(
+ f"{t('edge_relationship')}: {edge1_props.get('relationship', t('no_relationship'))}"
+ )
+ print(
+ f"{t('edge_desc')}: {edge1_props.get('description', t('no_description'))}"
+ )
+
+ # Verify edge relationship is correctly saved / 验证边关系是否正确保存
+ assert edge1_props.get("relationship") == edge1_data["relationship"], t(
+ "edge_relationship_mismatch"
+ ).format(edge1_data["relationship"], edge1_props.get("relationship"))
+
+ # Verify edge description is correctly saved / 验证边描述是否正确保存
+ assert edge1_props.get("description") == edge1_data["description"], t(
+ "edge_description_mismatch"
+ ).format(edge1_data["description"], edge1_props.get("description"))
+
+ print(
+ t("edge_special_char_verification_success").format(node1_id, node2_id)
+ )
+ else:
+ print(f"{t('read_edge_props_failed')}: {node1_id} -> {node2_id}")
+ assert False, t("unable_to_read_edge_props").format(node1_id, node2_id)
+
+ edge2_props = await storage.get_edge(node2_id, node3_id)
+ if edge2_props:
+ print(f"{t('read_edge_success')}: {node2_id} -> {node3_id}")
+ print(
+ f"{t('edge_relationship')}: {edge2_props.get('relationship', t('no_relationship'))}"
+ )
+ print(
+ f"{t('edge_desc')}: {edge2_props.get('description', t('no_description'))}"
+ )
+
+ # Verify edge relationship is correctly saved / 验证边关系是否正确保存
+ assert edge2_props.get("relationship") == edge2_data["relationship"], t(
+ "edge_relationship_mismatch"
+ ).format(edge2_data["relationship"], edge2_props.get("relationship"))
+
+ # Verify edge description is correctly saved / 验证边描述是否正确保存
+ assert edge2_props.get("description") == edge2_data["description"], t(
+ "edge_description_mismatch"
+ ).format(edge2_data["description"], edge2_props.get("description"))
+
+ print(
+ t("edge_special_char_verification_success").format(node2_id, node3_id)
+ )
+ else:
+ print(f"{t('read_edge_props_failed')}: {node2_id} -> {node3_id}")
+ assert False, t("unable_to_read_edge_props").format(node2_id, node3_id)
+
+ ASCIIColors.green(t("special_char_test_complete"))
+ return True
+
+ except Exception as e:
+ ASCIIColors.red(f"{t('test_error')}: {str(e)}")
+ return False
+
+
+# For direct execution/testing
+async def main():
+ """Test function for direct execution"""
+ from ..core.storage_setup import initialize_graph_test_storage
+
+ print(t("starting_special_character_test"))
+ storage = await initialize_graph_test_storage()
+
+ if storage:
+ result = await test_graph_special_characters(storage)
+ print(f"Test result: {result}")
+ else:
+ print("Storage initialization failed")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/tests/graph/tests/undirected.py b/tests/graph/tests/undirected.py
new file mode 100644
index 0000000000..db1f01f317
--- /dev/null
+++ b/tests/graph/tests/undirected.py
@@ -0,0 +1,189 @@
+"""
+Undirected graph property test module for graph storage.
+Tests the undirected behavior of the graph storage implementation.
+"""
+
+import asyncio
+from ..core.translation_engine import t_enhanced as t
+from ascii_colors import ASCIIColors
+
+
+async def test_graph_undirected_property(storage):
+ """
+ Test undirected graph property of storage (bilingual).
+ """
+ try:
+ # 1. Insert test data
+ node1_id = t("computer_science")
+ node1_data = {
+ "entity_id": node1_id,
+ "description": t("cs_desc"),
+ "keywords": t("cs_keywords"),
+ "entity_type": t("subject"),
+ }
+ print(f"{t('insert_node_1')}: {node1_id}")
+ await storage.upsert_node(node1_id, node1_data)
+
+ node2_id = t("data_structure")
+ node2_data = {
+ "entity_id": node2_id,
+ "description": t("ds_desc"),
+ "keywords": t("ds_keywords"),
+ "entity_type": t("concept"),
+ }
+ print(f"{t('insert_node_2')}: {node2_id}")
+ await storage.upsert_node(node2_id, node2_data)
+
+ node3_id = t("algorithm")
+ node3_data = {
+ "entity_id": node3_id,
+ "description": t("algo_desc"),
+ "keywords": t("algo_keywords"),
+ "entity_type": t("concept"),
+ }
+ print(f"{t('insert_node_3')}: {node3_id}")
+ await storage.upsert_node(node3_id, node3_data)
+
+ # 2. Test undirected property after inserting edge
+ print(t("test_insert_edge_undirected_property"))
+ edge1_data = {
+ "relationship": t("contains"),
+ "weight": 1.0,
+ "description": t("cs_contains_ds"),
+ }
+ print(f"{t('insert_edge_1')}: {node1_id} -> {node2_id}")
+ await storage.upsert_edge(node1_id, node2_id, edge1_data)
+
+ forward_edge = await storage.get_edge(node1_id, node2_id)
+ print(f"{t('forward_edge_props')}: {forward_edge}")
+ assert (
+ forward_edge is not None
+ ), f"{t('unable_read_edge')}: {node1_id} -> {node2_id}"
+
+ reverse_edge = await storage.get_edge(node2_id, node1_id)
+ print(f"{t('reverse_edge_props')}: {reverse_edge}")
+ assert (
+ reverse_edge is not None
+ ), f"{t('unable_read_reverse_edge')}: {node2_id} -> {node1_id}"
+
+ assert forward_edge == reverse_edge, t("forward_reverse_inconsistent")
+ print(t("undirected_verification_success"))
+
+ # 3. Test edge degree undirected property
+ print(t("test_edge_degree_undirected_property"))
+ edge2_data = {
+ "relationship": t("contains"),
+ "weight": 1.0,
+ "description": t("cs_contains_algo"),
+ }
+ print(f"{t('insert_edge_2')}: {node1_id} -> {node3_id}")
+ await storage.upsert_edge(node1_id, node3_id, edge2_data)
+
+ forward_degree = await storage.edge_degree(node1_id, node2_id)
+ reverse_degree = await storage.edge_degree(node2_id, node1_id)
+ print(f"{t('forward_edge_degree')}: {node1_id} -> {node2_id}: {forward_degree}")
+ print(f"{t('reverse_edge_degree')}: {node2_id} -> {node1_id}: {reverse_degree}")
+ assert forward_degree == reverse_degree, t("forward_reverse_inconsistent")
+ print(t("undirected_edge_degree_verification_success"))
+
+ # 4. Test undirected property after deleting edge
+ print(t("test_delete_edge_undirected_property"))
+ print(f"{t('delete_edge')}: {node1_id} -> {node2_id}")
+ await storage.remove_edges([(node1_id, node2_id)])
+
+ forward_edge = await storage.get_edge(node1_id, node2_id)
+ print(
+ f"{t('query_forward_edge_after_delete')}: {node1_id} -> {node2_id}: {forward_edge}"
+ )
+ assert (
+ forward_edge is None
+ ), f"{t('edge_should_be_deleted')}: {node1_id} -> {node2_id}"
+
+ reverse_edge = await storage.get_edge(node2_id, node1_id)
+ print(
+ f"{t('query_reverse_edge_after_delete')}: {node2_id} -> {node1_id}: {reverse_edge}"
+ )
+ assert reverse_edge is None, t("reverse_edge_should_be_deleted")
+ print(t("undirected_delete_verification_success"))
+
+ # 5. Test batch undirected property
+ print(t("test_batch_undirected_property"))
+ await storage.upsert_edge(node1_id, node2_id, edge1_data)
+ edge_dicts = [
+ {"src": node1_id, "tgt": node2_id},
+ {"src": node1_id, "tgt": node3_id},
+ ]
+ reverse_edge_dicts = [
+ {"src": node2_id, "tgt": node1_id},
+ {"src": node3_id, "tgt": node1_id},
+ ]
+ edges_dict = await storage.get_edges_batch(edge_dicts)
+ reverse_edges_dict = await storage.get_edges_batch(reverse_edge_dicts)
+ print(f"{t('batch_get_edges_result')}: {edges_dict.keys()}")
+ print(f"{t('batch_get_reverse_edges_result')}: {reverse_edges_dict.keys()}")
+ for (src, tgt), props in edges_dict.items():
+ assert (
+ (
+ tgt,
+ src,
+ )
+ in reverse_edges_dict
+ ), f"{t('reverse_edge_should_be_in_result')}: {tgt} -> {src}"
+ assert props == reverse_edges_dict[(tgt, src)], t(
+ "forward_reverse_inconsistent"
+ )
+ print(t("undirected_batch_verification_success"))
+
+ # 6. Test batch get node edges undirected property
+ print(t("test_batch_get_node_edges_undirected_property"))
+ nodes_edges = await storage.get_nodes_edges_batch([node1_id, node2_id])
+ print(f"{t('batch_get_nodes_edges_result')}: {nodes_edges.keys()}")
+ node1_edges = nodes_edges[node1_id]
+ node2_edges = nodes_edges[node2_id]
+ has_edge_to_node2 = any(
+ (src == node1_id and tgt == node2_id) for src, tgt in node1_edges
+ )
+ has_edge_to_node3 = any(
+ (src == node1_id and tgt == node3_id) for src, tgt in node1_edges
+ )
+ assert (
+ has_edge_to_node2
+ ), f"{t('node_edge_should_contain')}: {node1_id} -> {node2_id}"
+ assert (
+ has_edge_to_node3
+ ), f"{t('node_edge_should_contain')}: {node1_id} -> {node3_id}"
+ has_edge_to_node1 = any(
+ (src == node2_id and tgt == node1_id)
+ or (src == node1_id and tgt == node2_id)
+ for src, tgt in node2_edges
+ )
+ assert (
+ has_edge_to_node1
+ ), f"{t('node_edge_should_contain_connection')}: {node2_id} <-> {node1_id}"
+ print(t("undirected_nodes_edges_verification_success"))
+
+ ASCIIColors.green(t("undirected_test_complete"))
+ return True
+
+ except Exception as e:
+ ASCIIColors.red(f"{t('test_error')}: {str(e)}")
+ return False
+
+
+# For direct execution/testing
+async def main():
+ """Test function for direct execution"""
+ from ..core.storage_setup import initialize_graph_test_storage
+
+ print(t("starting_undirected_graph_test"))
+ storage = await initialize_graph_test_storage()
+
+ if storage:
+ result = await test_graph_undirected_property(storage)
+ print(f"Test result: {result}")
+ else:
+ print("Storage initialization failed")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/tests/graph/translations/__init__.py b/tests/graph/translations/__init__.py
new file mode 100644
index 0000000000..bc8620688e
--- /dev/null
+++ b/tests/graph/translations/__init__.py
@@ -0,0 +1,30 @@
+"""
+Translation utilities for multilingual support
+"""
+
+from .common import COMMON_TRANSLATIONS
+from .basic_test import BASIC_TEST_TRANSLATIONS
+from .advanced_test import ADVANCED_TEST_TRANSLATIONS
+from .batch_test import BATCH_TEST_TRANSLATIONS
+from .special_char_test import SPECIAL_CHAR_TEST_TRANSLATIONS
+from .undirected_test import UNDIRECTED_TEST_TRANSLATIONS
+from .utility import UTILITY_TRANSLATIONS
+
+
+def get_all_translations():
+ """
+ Combine all translations into a single dictionary organized by test function
+ """
+ return {
+ "common": COMMON_TRANSLATIONS,
+ "test_graph_basic": BASIC_TEST_TRANSLATIONS,
+ "test_graph_advanced": ADVANCED_TEST_TRANSLATIONS,
+ "test_graph_batch_operations": BATCH_TEST_TRANSLATIONS,
+ "test_graph_special_characters": SPECIAL_CHAR_TEST_TRANSLATIONS,
+ "test_graph_undirected_property": UNDIRECTED_TEST_TRANSLATIONS,
+ "test_check_env_file": UTILITY_TRANSLATIONS,
+ "setup_kuzu_test_environment": UTILITY_TRANSLATIONS,
+ "cleanup_kuzu_test_environment": UTILITY_TRANSLATIONS,
+ "initialize_graph_test_storage": UTILITY_TRANSLATIONS,
+ # Additional test translations will be added here as we create them
+ }
diff --git a/tests/graph/translations/advanced_test.py b/tests/graph/translations/advanced_test.py
new file mode 100644
index 0000000000..2173425003
--- /dev/null
+++ b/tests/graph/translations/advanced_test.py
@@ -0,0 +1,154 @@
+"""
+Translations specific to advanced graph testing
+"""
+
+ADVANCED_TEST_TRANSLATIONS = {
+ # Node data for advanced tests
+ "ai_keywords": [
+ "人工智能,机器学习,深度学习,神经网络,算法",
+ "artificial intelligence,machine learning,deep learning,neural networks,algorithms",
+ ],
+ "ml_desc": [
+ "机器学习是人工智能的一个子领域,使计算机能够在没有明确编程的情况下学习和改进。",
+ "Machine Learning is a subset of artificial intelligence that enables computers to learn and improve without being explicitly programmed.",
+ ],
+ "ml_keywords": [
+ "机器学习,监督学习,无监督学习,强化学习,算法",
+ "machine learning,supervised learning,unsupervised learning,reinforcement learning,algorithms",
+ ],
+ "dl_desc": [
+ "深度学习是机器学习的一个子领域,使用具有多层的神经网络来模拟人脑的学习过程。",
+ "Deep Learning is a subset of machine learning that uses neural networks with multiple layers to mimic the human brain's learning process.",
+ ],
+ "dl_keywords": [
+ "深度学习,神经网络,卷积神经网络,循环神经网络,人工神经网络",
+ "deep learning,neural networks,convolutional neural networks,recurrent neural networks,artificial neural networks",
+ ],
+ "contains": [
+ "包含",
+ "contains",
+ ],
+ "ai_contains_ml": [
+ "人工智能领域包含机器学习这个重要的子领域",
+ "The field of artificial intelligence contains machine learning as an important subfield",
+ ],
+ "ml_contains_dl": [
+ "机器学习领域包含深度学习这个重要的子领域",
+ "The field of machine learning contains deep learning as an important subfield",
+ ],
+ # Advanced test operations
+ "test_node_degree": ["测试 node_degree", "Test node_degree"],
+ "test_all_node_degrees": ["测试所有节点的度数", "Test all node degrees"],
+ "test_edge_degree": ["测试 edge_degree", "Test edge_degree"],
+ "test_reverse_edge_degree": ["测试反向边的度数", "Test reverse edge degree"],
+ "test_get_node_edges": ["测试 get_node_edges", "Test get_node_edges"],
+ "test_get_all_labels": ["== 测试 get_all_labels ==", "== Test get_all_labels =="],
+ "test_get_knowledge_graph": [
+ "== 测试 get_knowledge_graph ==",
+ "== Test get_knowledge_graph ==",
+ ],
+ "test_delete_node": ["测试 delete_node", "Test delete_node"],
+ "test_remove_edges": ["测试 remove_edges", "Test remove_edges"],
+ "test_remove_nodes": ["测试 remove_nodes", "Test remove_nodes"],
+ # Verification operations
+ "verify_undirected_property": [
+ "验证无向图特性",
+ "Verify undirected graph property",
+ ],
+ "verify_node_edges_undirected": [
+ "验证节点边的无向图特性",
+ "Verify node edges undirected property",
+ ],
+ # Display messages
+ "node_degree": ["节点度数", "Node degree"],
+ "edge_degree": ["边度数", "Edge degree"],
+ "reverse_edge_degree": ["反向边度数", "Reverse edge degree"],
+ "all_edges": ["所有边", "All edges"],
+ "all_labels": ["所有标签", "All labels"],
+ "knowledge_graph_nodes": ["知识图谱节点数", "Knowledge graph nodes"],
+ "knowledge_graph_edges": ["知识图谱边数", "Knowledge graph edges"],
+ "query_after_deletion": ["删除后查询", "Query after deletion"],
+ "re_insert_for_test": ["重新插入用于后续测试", "Re-insert for subsequent testing"],
+ # Assertion messages using %-style formatting
+ "edge_degree_should_be": [
+ "边 %s -> %s 的度数应为%d,实际为 %d",
+ "Edge %s -> %s degree should be %d, actual %d",
+ ],
+ "forward_reverse_edge_inconsistent": [
+ "正向和反向边属性不一致",
+ "Forward and reverse edge properties are inconsistent",
+ ],
+ "node_edge_should_contain_connection": [
+ "节点 %s 的边列表中应包含与 %s 的连接",
+ "Node %s edge list should contain connection with %s",
+ ],
+ "node_should_be_deleted": [
+ "节点 %s 应被删除",
+ "Node %s should be deleted",
+ ],
+ "edge_should_be_deleted": [
+ "边 %s -> %s 应被删除",
+ "Edge %s -> %s should be deleted",
+ ],
+ "reverse_edge_should_be_deleted": [
+ "反向边 %s -> %s 应被删除",
+ "Reverse edge %s -> %s should be deleted",
+ ],
+ "should_have_labels": [
+ "应有%d个标签,实际有 %d",
+ "Should have %d labels, actual %d",
+ ],
+ "should_be_in_label_list": [
+ "%s 应在标签列表中",
+ "%s should be in label list",
+ ],
+ "result_should_be_kg_type": [
+ "返回结果应为 KnowledgeGraph 类型",
+ "Result should be KnowledgeGraph type",
+ ],
+ "kg_should_have_nodes": [
+ "知识图谱应有%d个节点,实际有 %d",
+ "Knowledge graph should have %d nodes, actual %d",
+ ],
+ "kg_should_have_edges": [
+ "知识图谱应有%d条边,实际有 %d",
+ "Knowledge graph should have %d edges, actual %d",
+ ],
+ "node_degree_display": [
+ "节点 %s 的度数: %d",
+ "Node %s degree: %d",
+ ],
+ "edge_degree_display": [
+ "边 %s -> %s 的度数: %d",
+ "Edge %s -> %s degree: %d",
+ ],
+ "undirected_node_edges_success": [
+ "无向图特性验证成功:节点 %s 的边列表包含所有相关的边",
+ "Undirected graph property verification successful: node %s edge list contains all related edges",
+ ],
+ "query_after_deletion_display": [
+ "删除后查询节点属性 %s: %s",
+ "Query after deletion %s: %s",
+ ],
+ "query_edge_after_deletion_display": [
+ "删除后查询边属性 %s -> %s: %s",
+ "Query after deletion %s -> %s: %s",
+ ],
+ # Test completion
+ "advanced_test_complete": [
+ "\n高级测试完成",
+ "\nAdvanced test completed",
+ ],
+ "starting_advanced_test": [
+ "\n=== 开始高级测试 ===",
+ "\n=== Starting Advanced Test ===",
+ ],
+ "test_error": [
+ "测试错误",
+ "Test error",
+ ],
+ "query_reverse_edge_after_deletion": [
+ "删除后查询反向边属性 %s -> %s: %s",
+ "Query after deletion reverse edge %s -> %s: %s",
+ ],
+}
diff --git a/tests/graph/translations/basic_test.py b/tests/graph/translations/basic_test.py
new file mode 100644
index 0000000000..8083cfc832
--- /dev/null
+++ b/tests/graph/translations/basic_test.py
@@ -0,0 +1,60 @@
+"""
+Translations specific to basic graph testing
+"""
+
+BASIC_TEST_TRANSLATIONS = {
+ # Basic test specific messages
+ "success_read_node": ["成功读取节点属性", "Successfully read node properties"],
+ "success_read_edge": ["成功读取边属性", "Successfully read edge properties"],
+ "success_read_reverse": [
+ "成功读取反向边属性",
+ "Successfully read reverse edge properties",
+ ],
+ "failed_read_node": ["读取节点属性失败", "Failed to read node properties"],
+ "failed_read_edge": ["读取边属性失败", "Failed to read edge properties"],
+ "failed_read_reverse_edge": [
+ "读取反向边属性失败",
+ "Failed to read reverse edge properties",
+ ],
+ "unable_read_node": ["未能读取节点属性", "Unable to read node properties"],
+ "unable_read_edge": ["未能读取边属性", "Unable to read edge properties"],
+ "unable_read_reverse_edge": [
+ "未能读取反向边属性",
+ "Unable to read reverse edge properties",
+ ],
+ # Property descriptions
+ "node_desc": ["节点描述", "Node description"],
+ "node_type": ["节点类型", "Node type"],
+ "node_keywords": ["节点关键词", "Node keywords"],
+ "edge_relation": ["边关系", "Edge relationship"],
+ "edge_desc": ["边描述", "Edge description"],
+ "edge_weight": ["边权重", "Edge weight"],
+ "reverse_edge_relation": ["反向边关系", "Reverse edge relationship"],
+ "reverse_edge_desc": ["反向边描述", "Reverse edge description"],
+ "reverse_edge_weight": ["反向边权重", "Reverse edge weight"],
+ # Validation messages
+ "node_id_mismatch": ["节点ID不匹配: 期望", "Node ID mismatch: expected"],
+ "node_desc_mismatch": ["节点描述不匹配", "Node description mismatch"],
+ "node_type_mismatch": ["节点类型不匹配", "Node type mismatch"],
+ "edge_relation_mismatch": ["边关系不匹配", "Edge relationship mismatch"],
+ "edge_desc_mismatch": ["边描述不匹配", "Edge description mismatch"],
+ "edge_weight_mismatch": ["边权重不匹配", "Edge weight mismatch"],
+ # Undirected graph verification
+ "forward_reverse_inconsistent": [
+ "正向和反向边属性不一致,无向图特性验证失败",
+ "Forward and reverse edge properties inconsistent, undirected graph property verification failed",
+ ],
+ "undirected_verification_failed": [
+ "无向图特性验证失败",
+ "undirected graph property verification failed",
+ ],
+ # Test completion
+ "basic_test_complete": [
+ "\n基本测试完成,数据已保留在数据库中",
+ "\nBasic test completed, data retained in database",
+ ],
+ "starting_basic_test": [
+ "\n=== 开始基本测试 ===",
+ "\n=== Starting Basic Test ===",
+ ],
+}
diff --git a/tests/graph/translations/batch_test.py b/tests/graph/translations/batch_test.py
new file mode 100644
index 0000000000..08b31a89c1
--- /dev/null
+++ b/tests/graph/translations/batch_test.py
@@ -0,0 +1,177 @@
+"""
+Translations specific to batch operations testing
+"""
+
+BATCH_TEST_TRANSLATIONS = {
+ # Batch operation tests
+ "batch_get_nodes": ["== 测试 get_nodes_batch ==", "== Test get_nodes_batch =="],
+ "batch_node_degrees": [
+ "== 测试 node_degrees_batch ==",
+ "== Test node_degrees_batch ==",
+ ],
+ "batch_edge_degrees": [
+ "== 测试 edge_degrees_batch ==",
+ "== Test edge_degrees_batch ==",
+ ],
+ "batch_get_edges": ["== 测试 get_edges_batch ==", "== Test get_edges_batch =="],
+ "test_reverse_edges_batch": [
+ "== 测试反向边的批量获取 ==",
+ "== Test reverse edges batch get ==",
+ ],
+ "test_get_nodes_edges_batch": [
+ "=== 测试 get_nodes_edges_batch ===",
+ "=== Test get_nodes_edges_batch ===",
+ ],
+ "verify_batch_nodes_edges_undirected": [
+ "=== 验证批量获取节点边的无向图特性 ===",
+ "=== Verify batch get node edges undirected graph property ===",
+ ],
+ "test_get_nodes_by_chunk_ids": [
+ "== 测试 get_nodes_by_chunk_ids ==",
+ "== Test get_nodes_by_chunk_ids ==",
+ ],
+ "test_single_chunk_id_multiple_nodes": [
+ "== 测试单个 chunk_id,匹配多个节点 ==",
+ "== Test single chunk_id, matching multiple nodes ==",
+ ],
+ "test_multiple_chunk_ids_partial_match": [
+ "== 测试多个 chunk_id,部分匹配多个节点 ==",
+ "== Test multiple chunk_ids, partial matching multiple nodes ==",
+ ],
+ "test_get_edges_by_chunk_ids": [
+ "== 测试 get_edges_by_chunk_ids ==",
+ "== Test get_edges_by_chunk_ids ==",
+ ],
+ "test_single_chunk_id_multiple_edges": [
+ "== 测试单个 chunk_id,匹配多条边 ==",
+ "== Test single chunk_id, matching multiple edges ==",
+ ],
+ "test_multiple_chunk_ids_partial_edges": [
+ "== 测试多个 chunk_id,部分匹配多条边 ==",
+ "== Test multiple chunk_ids, partial matching multiple edges ==",
+ ],
+ # Results
+ "batch_get_nodes_result": [
+ "批量获取节点属性结果",
+ "Batch get node properties result",
+ ],
+ "batch_node_degrees_result": [
+ "批量获取节点度数结果",
+ "Batch get node degrees result",
+ ],
+ "batch_edge_degrees_result": [
+ "批量获取边度数结果",
+ "Batch get edge degrees result",
+ ],
+ # Insert messages
+ "insert_node_1": ["插入节点1", "Insert node 1"],
+ "insert_node_2": ["插入节点2", "Insert node 2"],
+ "insert_node_3": ["插入节点3", "Insert node 3"],
+ "insert_node_4": ["插入节点4", "Insert node 4"],
+ "insert_node_5": ["插入节点5", "Insert node 5"],
+ "insert_edge_1": ["插入边1", "Insert edge 1"],
+ "insert_edge_2": ["插入边2", "Insert edge 2"],
+ "insert_edge_3": ["插入边3", "Insert edge 3"],
+ "insert_edge_4": ["插入边4", "Insert edge 4"],
+ "insert_edge_5": ["插入边5", "Insert edge 5"],
+ "insert_edge_6": ["插入边6", "Insert edge 6"],
+ # Node/Edge Display
+ "node_outgoing_edges": ["的出边", "Node outgoing edges"],
+ "node_incoming_edges": ["的入边", "Node incoming edges"],
+ "node": ["节点", "Node"],
+ # Assert messages
+ "should_return_nodes": [
+ "应返回%d个节点,实际返回 %d 个",
+ "Should return %d nodes, actual %d",
+ ],
+ "should_return_node_degrees": [
+ "应返回%d个节点的度数,实际返回 %d 个",
+ "Should return %d node degrees, actual %d",
+ ],
+ "should_return_edge_degrees": [
+ "应返回%d条边的度数,实际返回 %d 条",
+ "Should return %d edge degrees, actual %d",
+ ],
+ "should_return_edge_properties": [
+ "应返回%d条边的属性,实际返回 %d 条",
+ "Should return %d edge properties, actual %d",
+ ],
+ "should_return_reverse_edge_properties": [
+ "应返回%d条反向边的属性,实际返回 %d 条",
+ "Should return %d reverse edge properties, actual %d",
+ ],
+ "should_return_node_edges": [
+ "应返回%d个节点的边,实际返回 %d 个",
+ "Should return %d node edges, actual %d",
+ ],
+ "should_be_in_result": [
+ "%s 应在返回结果中",
+ "%s should be in result",
+ ],
+ "edge_should_be_in_result": [
+ "边 %s -> %s 应在返回结果中",
+ "Edge %s -> %s should be in result",
+ ],
+ "reverse_edge_should_be_in_result": [
+ "反向边 %s -> %s 应在返回结果中",
+ "Reverse edge %s -> %s should be in result",
+ ],
+ "node_should_be_in_result": [
+ "节点 %s 应在返回结果中",
+ "Node %s should be in result",
+ ],
+ "node_edge_list_should_contain_edge_to": [
+ "节点 %s 的边列表中应包含到 %s 的边",
+ "Node %s edge list should contain edge to %s",
+ ],
+ "node_should_have_edges_count": [
+ "%s 应有%d条边,实际有 %d 条",
+ "%s should have %d edges, actual %d",
+ ],
+ "chunk_should_have_nodes": [
+ "%s 应有%d个节点,实际有 %d 个",
+ "%s should have %d nodes, actual %d",
+ ],
+ "chunk_should_have_edges": [
+ "%s 应有%d条边,实际有 %d 条",
+ "%s should have %d edges, actual %d",
+ ],
+ "chunks_should_have_nodes": [
+ "%s, %s 应有%d个节点,实际有 %d 个",
+ "%s, %s should have %d nodes, actual %d",
+ ],
+ "chunks_should_have_edges": [
+ "%s, %s 应有%d条边,实际有 %d 条",
+ "%s, %s should have %d edges, actual %d",
+ ],
+ "chunk_should_contain_edge": [
+ "%s 应包含 %s 到 %s 的边",
+ "%s should contain edge from %s to %s",
+ ],
+ "chunks_should_contain_edge": [
+ "%s, %s 应包含 %s 到 %s 的边",
+ "%s, %s should contain edge from %s to %s",
+ ],
+ "node_edge_list_should_contain_connection": [
+ "节点 %s 的边列表中应包含与 %s 的连接",
+ "Node %s edge list should contain connection with %s",
+ ],
+ "description_mismatch": [
+ "%s 描述不匹配",
+ "%s description mismatch",
+ ],
+ # Success messages
+ "undirected_batch_verification_success": [
+ "无向图特性验证成功:批量获取的正向和反向边属性一致",
+ "Undirected graph property verification successful: batch obtained forward and reverse edge properties are consistent",
+ ],
+ # Test completion
+ "batch_operations_test_complete": [
+ "\n批量操作测试完成",
+ "\nBatch operations test completed",
+ ],
+ "starting_batch_operations_test": [
+ "\n=== 开始批量操作测试 ===",
+ "\n=== Starting Batch Operations Test ===",
+ ],
+}
diff --git a/tests/graph/translations/common.py b/tests/graph/translations/common.py
new file mode 100644
index 0000000000..f744098d68
--- /dev/null
+++ b/tests/graph/translations/common.py
@@ -0,0 +1,202 @@
+"""
+Common translations used across multiple tests
+"""
+
+COMMON_TRANSLATIONS = {
+ # Program header
+ "program_title": [
+ """
+ ╔══════════════════════════════════════════════════════════════╗
+ ║ 通用图存储测试程序 ║
+ ╚══════════════════════════════════════════════════════════════╝
+ """,
+ """
+ ╔══════════════════════════════════════════════════════════════╗
+ ║ Universal Graph Storage Test ║
+ ╚══════════════════════════════════════════════════════════════╝
+ """,
+ ],
+ # Basic elements
+ "node": ["节点", "Node"],
+ "edge": ["边", "Edge"],
+ "degree": ["度数", "Degree"],
+ "delete_edge": ["删除边", "Delete Edge"],
+ # Basic operations
+ "insert_node": ["插入节点", "Insert node"],
+ "insert_edge": ["插入边", "Insert edge"],
+ "read_node_props": ["读取节点属性", "Read node properties"],
+ "read_edge_props": ["读取边属性", "Read edge properties"],
+ "read_reverse_edge": ["读取反向边属性", "Read reverse edge properties"],
+ # Entity data
+ "artificial_intelligence": ["人工智能", "Artificial Intelligence"],
+ "machine_learning": ["机器学习", "Machine Learning"],
+ "deep_learning": ["深度学习", "Deep Learning"],
+ "natural_language_processing": ["自然语言处理", "Natural Language Processing"],
+ "computer_vision": ["计算机视觉", "Computer Vision"],
+ "computer_science": ["计算机科学", "Computer Science"],
+ "data_structure": ["数据结构", "Data Structure"],
+ "algorithm": ["算法", "Algorithm"],
+ # Descriptions
+ "ai_desc": [
+ "人工智能是计算机科学的一个分支,它企图了解智能的实质,并生产出一种新的能以人类智能相似的方式做出反应的智能机器。",
+ "Artificial Intelligence is a branch of computer science that attempts to understand the essence of intelligence and produce a new kind of intelligent machine that can respond in a way similar to human intelligence.",
+ ],
+ "ml_desc": [
+ "机器学习是人工智能的一个分支,它使用统计学方法让计算机系统在不被明确编程的情况下也能够学习。",
+ "Machine Learning is a branch of artificial intelligence that uses statistical methods to enable computer systems to learn without being explicitly programmed.",
+ ],
+ "dl_desc": [
+ "深度学习是机器学习的一个分支,它使用多层神经网络来模拟人脑的学习过程。",
+ "Deep Learning is a branch of machine learning that uses multi-layer neural networks to simulate the human brain's learning process.",
+ ],
+ "nlp_desc": [
+ "自然语言处理是人工智能的一个分支,专注于使计算机理解和处理人类语言。",
+ "Natural Language Processing is a branch of artificial intelligence that focuses on enabling computers to understand and process human language.",
+ ],
+ "cv_desc": [
+ "计算机视觉是人工智能的一个分支,专注于使计算机能够从图像或视频中获取信息。",
+ "Computer Vision is a branch of artificial intelligence that focuses on enabling computers to obtain information from images or videos.",
+ ],
+ "cs_desc": [
+ "计算机科学是研究计算机及其应用的科学。",
+ "Computer Science is the science that studies computers and their applications.",
+ ],
+ "ds_desc": [
+ "数据结构是计算机科学中的一个基础概念,用于组织和存储数据。",
+ "Data Structure is a fundamental concept in computer science, used to organize and store data.",
+ ],
+ "algo_desc": [
+ "算法是解决问题的步骤和方法。",
+ "Algorithm is the steps and methods for solving problems.",
+ ],
+ # Keywords
+ "ai_keywords": ["AI,机器学习,深度学习", "AI,machine learning,deep learning"],
+ "ml_keywords": [
+ "监督学习,无监督学习,强化学习",
+ "supervised learning,unsupervised learning,reinforcement learning",
+ ],
+ "dl_keywords": ["神经网络,CNN,RNN", "neural networks,CNN,RNN"],
+ "node_degree_should_be": [
+ "节点 %s 的度数应为%d,实际为 %d",
+ "Node %s degree should be %d, actual %d",
+ ],
+ "node_should_have_edges": [
+ "节点 %s 应有%d条边,实际有 %d",
+ "Node %s should have %d edges, actual %d",
+ ],
+ "undirected_verification_success": [
+ "无向图特性验证成功:正向和反向边属性一致",
+ "Undirected graph property verification successful: forward and reverse edge properties are consistent",
+ ],
+ "nlp_keywords": ["NLP,文本分析,语言模型", "NLP,text analysis,language models"],
+ "cv_keywords": ["CV,图像识别,目标检测", "CV,image recognition,object detection"],
+ "cs_keywords": ["计算机,科学,技术", "computer,science,technology"],
+ "ds_keywords": ["数据,结构,组织", "data,structure,organization"],
+ "algo_keywords": ["算法,步骤,方法", "algorithm,steps,methods"],
+ # Entity types
+ "tech_field": ["技术领域", "Technology Field"],
+ "concept": ["概念", "Concept"],
+ "subject": ["学科", "Subject"],
+ "test_node": ["测试节点", "Test Node"],
+ # Relationships
+ "contains": ["包含", "contains"],
+ "applied_to": ["应用于", "applied to"],
+ "special_relation": ["特殊'关系'", "special 'relation'"],
+ # Relationship descriptions
+ "ai_contains_ml": [
+ "人工智能领域包含机器学习这个子领域",
+ "The field of artificial intelligence contains machine learning as a subfield",
+ ],
+ "ml_contains_dl": [
+ "机器学习领域包含深度学习这个子领域",
+ "The field of machine learning contains deep learning as a subfield",
+ ],
+ "ai_contains_nlp": [
+ "人工智能领域包含自然语言处理这个子领域",
+ "The field of artificial intelligence contains natural language processing as a subfield",
+ ],
+ "ai_contains_cv": [
+ "人工智能领域包含计算机视觉这个子领域",
+ "The field of artificial intelligence contains computer vision as a subfield",
+ ],
+ "ai_contains_cv_desc": [
+ "人工智能领域包含计算机视觉这个子领域",
+ "The field of artificial intelligence contains computer vision as a subfield",
+ ],
+ "dl_applied_nlp": [
+ "深度学习技术应用于自然语言处理领域",
+ "Deep learning technology is applied to the field of natural language processing",
+ ],
+ "dl_applied_nlp_relationship": ["应用于", "applied to"],
+ "dl_applied_nlp_desc": [
+ "深度学习技术应用于自然语言处理领域",
+ "Deep learning technology is applied to the field of natural language processing",
+ ],
+ "dl_applied_cv": [
+ "深度学习技术应用于计算机视觉领域",
+ "Deep learning technology is applied to the field of computer vision",
+ ],
+ "dl_applied_cv_relationship": ["应用于", "applied to"],
+ "dl_applied_cv_desc": [
+ "深度学习技术应用于计算机视觉领域",
+ "Deep learning technology is applied to the field of computer vision",
+ ],
+ "cs_contains_ds": [
+ "计算机科学包含数据结构这个概念",
+ "Computer science contains data structures as a concept",
+ ],
+ "cs_contains_algo": [
+ "计算机科学包含算法这个概念",
+ "Computer science contains algorithms as a concept",
+ ],
+ # Common status messages
+ "no_type": ["无类型", "No type"],
+ "no_keywords": ["无关键词", "No keywords"],
+ "no_weight": ["无权重", "No weight"],
+ "actual": ["实际", "actual"],
+ "test_error": ["测试过程中发生错误", "Error occurred during testing"],
+ # System messages
+ "error": ["错误", "Error"],
+ "warning": ["警告", "Warning"],
+ "current_graph_storage": [
+ "当前配置的图存储类型",
+ "Current configured graph storage type",
+ ],
+ "supported_graph_storage": [
+ "支持的图存储类型",
+ "Supported graph storage types",
+ ],
+ "init_storage_failed": [
+ "初始化存储实例失败,测试程序退出",
+ "Failed to initialize storage instance, test program exiting",
+ ],
+ # Additional node operations
+ "insert_node_1": ["插入节点1", "Insert node 1"],
+ "insert_node_2": ["插入节点2", "Insert node 2"],
+ "insert_node_3": ["插入节点3", "Insert node 3"],
+ "insert_edge_1": ["插入边1", "Insert edge 1"],
+ "insert_edge_2": ["插入边2", "Insert edge 2"],
+ # Edge properties
+ "edge_desc": ["边描述", "Edge description"],
+ "edge_relationship": ["边关系", "Edge relationship"],
+ "edge_weight": ["边权重", "Edge weight"],
+ # Common descriptions
+ "no_description": ["无描述", "No description"],
+ "no_relationship": ["无关系", "No relationship"],
+ "batch_get_edges_result": [
+ "批量获取边属性结果",
+ "Batch get edge properties result",
+ ],
+ "batch_get_reverse_edges_result": [
+ "批量获取反向边属性结果",
+ "Batch get reverse edge properties result",
+ ],
+ "batch_get_nodes_edges_result": [
+ "批量获取节点边结果",
+ "Batch get node edges result",
+ ],
+ "undirected_nodes_edges_verification_success": [
+ "无向图特性验证成功:批量获取的节点边包含所有相关的边(无论方向)",
+ "Undirected graph property verification successful: batch obtained node edges contain all related edges (regardless of direction)",
+ ],
+}
diff --git a/tests/graph/translations/special_char_test.py b/tests/graph/translations/special_char_test.py
new file mode 100644
index 0000000000..bc481c2ca2
--- /dev/null
+++ b/tests/graph/translations/special_char_test.py
@@ -0,0 +1,147 @@
+"""
+Translations specific to special character testing
+"""
+
+SPECIAL_CHAR_TEST_TRANSLATIONS = {
+ # Special character nodes
+ "node_with_quotes": ["包含'单引号'的节点", "Node with 'single quotes'"],
+ "node_with_double_quotes": ['包含"双引号"的节点', 'Node with "double quotes"'],
+ "node_with_backslash": ["包含\\反斜杠\\的节点", "Node with \\backslash\\"],
+ # Special character descriptions
+ "desc_with_special": [
+ "这个描述包含'单引号'、\"双引号\"和\\反斜杠",
+ "This description contains 'single quotes', \"double quotes\" and \\backslash",
+ ],
+ "desc_with_complex": [
+ "这个描述同时包含'单引号'和\"双引号\"以及\\反斜杠\\路径",
+ "This description contains 'single quotes' and \"double quotes\" as well as \\backslash\\path",
+ ],
+ "desc_with_windows_path": [
+ "这个描述包含Windows路径C:\\Program Files\\和转义字符\\n\\t",
+ "This description contains Windows path C:\\Program Files\\ and escape characters \\n\\t",
+ ],
+ # Keywords with special characters
+ "keywords_special": [
+ "特殊字符,引号,转义",
+ "special characters,quotes,escape",
+ ],
+ "keywords_backslash": [
+ "反斜杠,路径,转义",
+ "backslash,path,escape",
+ ],
+ "keywords_json": [
+ "特殊字符,引号,JSON",
+ "special characters,quotes,JSON",
+ ],
+ # Edge descriptions with special characters
+ "edge_desc_special": [
+ "这个边描述包含'单引号'、\"双引号\"和\\反斜杠",
+ "This edge description contains 'single quotes', \"double quotes\" and \\backslash",
+ ],
+ "edge_desc_sql": [
+ "包含SQL注入尝试: SELECT * FROM users WHERE name='admin'--",
+ "Contains SQL injection attempt: SELECT * FROM users WHERE name='admin'--",
+ ],
+ # Relations with special characters
+ "complex_relation": [
+ "复杂'关系'包含\\转义",
+ "complex 'relation' with \\escape",
+ ],
+ # Test operations
+ "insert_node_with_special_1": [
+ "插入包含特殊字符的节点1",
+ "Insert node 1 with special characters",
+ ],
+ "insert_node_with_special_2": [
+ "插入包含特殊字符的节点2",
+ "Insert node 2 with special characters",
+ ],
+ "insert_node_with_special_3": [
+ "插入包含特殊字符的节点3",
+ "Insert node 3 with special characters",
+ ],
+ "insert_edge_with_special": [
+ "插入包含特殊字符的边",
+ "Insert edge with special characters",
+ ],
+ "insert_edge_with_complex_special": [
+ "插入包含复杂特殊字符的边",
+ "Insert edge with complex special characters",
+ ],
+ # Read operations
+ "read_node_success": [
+ "成功读取节点",
+ "Successfully read node",
+ ],
+ "read_edge_success": [
+ "成功读取边",
+ "Successfully read edge",
+ ],
+ "read_node_props_failed": [
+ "读取节点属性失败",
+ "Failed to read node properties",
+ ],
+ "read_edge_props_failed": [
+ "读取边属性失败",
+ "Failed to read edge properties",
+ ],
+ # Property display
+ "node_description": [
+ "节点描述",
+ "Node description",
+ ],
+ "edge_relationship": [
+ "边关系",
+ "Edge relationship",
+ ],
+ # Verification
+ "verify_node_special": ["验证节点特殊字符", "Verify node special characters"],
+ "verify_edge_special": ["验证边特殊字符", "Verify edge special characters"],
+ "special_char_verification_success": [
+ "特殊字符验证成功",
+ "Special character verification successful",
+ ],
+ # Verification messages
+ "node_special_char_verification_success": [
+ "节点 {} 特殊字符验证成功",
+ "Node {} special character verification successful",
+ ],
+ "edge_special_char_verification_success": [
+ "边 {} -> {} 特殊字符验证成功",
+ "Edge {} -> {} special character verification successful",
+ ],
+ # Error messages
+ "node_id_mismatch_f": [
+ "节点ID不匹配: 期望 {}, 实际 {}",
+ "Node ID mismatch: expected {}, actual {}",
+ ],
+ "node_description_mismatch": [
+ "节点描述不匹配: 期望 {}, 实际 {}",
+ "Node description mismatch: expected {}, actual {}",
+ ],
+ "edge_relationship_mismatch": [
+ "边关系不匹配: 期望 {}, 实际 {}",
+ "Edge relationship mismatch: expected {}, actual {}",
+ ],
+ "edge_description_mismatch": [
+ "边描述不匹配: 期望 {}, 实际 {}",
+ "Edge description mismatch: expected {}, actual {}",
+ ],
+ "unable_to_read_node_props": [
+ "未能读取节点属性: {}",
+ "Unable to read node properties: {}",
+ ],
+ "unable_to_read_edge_props": [
+ "未能读取边属性: {} -> {}",
+ "Unable to read edge properties: {} -> {}",
+ ],
+ # Test completion
+ "special_char_test_complete": [
+ "特殊字符测试完成",
+ "Special character test completed",
+ ],
+ "starting_special_character_test": [
+ "\n=== 开始特殊字符测试 ===",
+ "\n=== Starting Special Character Test ===",
+ ],
+}
diff --git a/tests/graph/translations/undirected_test.py b/tests/graph/translations/undirected_test.py
new file mode 100644
index 0000000000..845fbe0be1
--- /dev/null
+++ b/tests/graph/translations/undirected_test.py
@@ -0,0 +1,145 @@
+"""
+Translations specific to undirected graph property testing
+"""
+
+UNDIRECTED_TEST_TRANSLATIONS = {
+ # Node descriptions
+ "cs_desc": [
+ "计算机科学是研究计算理论、算法设计和计算机系统设计的学科",
+ "Computer Science is the study of computation theory, algorithm design, and computer system design",
+ ],
+ "ds_desc": [
+ "数据结构是计算机中存储、组织数据的方式",
+ "Data structure is the way to store and organize data in computers",
+ ],
+ "algo_desc": [
+ "算法是解决问题的一系列有序的计算步骤",
+ "Algorithm is a series of ordered computational steps to solve problems",
+ ],
+ # Node keywords
+ "cs_keywords": [
+ "计算机,科学,编程",
+ "computer,science,programming",
+ ],
+ "ds_keywords": [
+ "数据,结构,存储",
+ "data,structure,storage",
+ ],
+ "algo_keywords": [
+ "算法,计算,步骤",
+ "algorithm,computation,steps",
+ ],
+ # Entity types
+ "subject": ["学科", "Subject"],
+ "concept": ["概念", "Concept"],
+ # Edge descriptions
+ "cs_contains_ds": [
+ "计算机科学领域包含数据结构概念",
+ "Computer Science field contains Data Structure concepts",
+ ],
+ "cs_contains_algo": [
+ "计算机科学领域包含算法概念",
+ "Computer Science field contains Algorithm concepts",
+ ],
+ # Test operations
+ "test_insert_edge_undirected_property": [
+ "测试插入边的无向图特性",
+ "Test insert edge undirected property",
+ ],
+ "test_edge_degree_undirected_property": [
+ "测试边度数的无向图特性",
+ "Test edge degree undirected property",
+ ],
+ "test_delete_edge_undirected_property": [
+ "测试删除边的无向图特性",
+ "Test delete edge undirected property",
+ ],
+ "test_batch_undirected_property": [
+ "测试批量操作的无向图特性",
+ "Test batch operations undirected property",
+ ],
+ "test_batch_get_node_edges_undirected_property": [
+ "测试批量获取节点边的无向图特性",
+ "Test batch get node edges undirected property",
+ ],
+ # Edge operations
+ "forward_edge_props": [
+ "正向边属性",
+ "Forward edge properties",
+ ],
+ "reverse_edge_props": [
+ "反向边属性",
+ "Reverse edge properties",
+ ],
+ "forward_edge_degree": [
+ "正向边度数",
+ "Forward edge degree",
+ ],
+ "reverse_edge_degree": [
+ "反向边度数",
+ "Reverse edge degree",
+ ],
+ "delete_edge": [
+ "删除边",
+ "Delete edge",
+ ],
+ "query_forward_edge_after_delete": [
+ "删除后查询正向边",
+ "Query forward edge after delete",
+ ],
+ "query_reverse_edge_after_delete": [
+ "删除后查询反向边",
+ "Query reverse edge after delete",
+ ],
+ # Error messages
+ "unable_read_edge": [
+ "无法读取边",
+ "Unable to read edge",
+ ],
+ "unable_read_reverse_edge": [
+ "无法读取反向边",
+ "Unable to read reverse edge",
+ ],
+ "forward_reverse_inconsistent": [
+ "正向和反向边不一致",
+ "Forward and reverse edges are inconsistent",
+ ],
+ "reverse_edge_should_be_deleted": [
+ "反向边应该被删除",
+ "Reverse edge should be deleted",
+ ],
+ "reverse_edge_should_be_in_result": [
+ "反向边应该在结果中",
+ "Reverse edge should be in result",
+ ],
+ "node_edge_should_contain": [
+ "节点边应该包含",
+ "Node edge should contain",
+ ],
+ "node_edge_should_contain_connection": [
+ "节点边应该包含连接",
+ "Node edge should contain connection",
+ ],
+ # Verification messages
+ "undirected_edge_degree_verification_success": [
+ "无向图边度数验证成功",
+ "Undirected graph edge degree verification successful",
+ ],
+ "undirected_delete_verification_success": [
+ "无向图删除验证成功",
+ "Undirected graph deletion verification successful",
+ ],
+ "undirected_batch_verification_success": [
+ "无向图批量操作验证成功",
+ "Undirected graph batch operations verification successful",
+ ],
+ # Test completion
+ "undirected_test_complete": [
+ "无向图特性测试完成",
+ "Undirected graph property test completed",
+ ],
+ "starting_undirected_graph_test": [
+ "\n=== 开始无向图特性测试 ===",
+ "\n=== Starting Undirected Graph Property Test ===",
+ ],
+}
diff --git a/tests/graph/translations/utility.py b/tests/graph/translations/utility.py
new file mode 100644
index 0000000000..0c0080f4b1
--- /dev/null
+++ b/tests/graph/translations/utility.py
@@ -0,0 +1,55 @@
+"""
+Utility function translations
+工具函数翻译
+"""
+
+UTILITY_TRANSLATIONS = {
+ # Environment file checking
+ "warning_no_env": [
+ "警告: 当前目录中没有找到.env文件,这可能会影响存储配置的加载。",
+ "Warning: No .env file found in the current directory, which may affect storage configuration loading.",
+ ],
+ "continue_execution": [
+ "是否继续执行? (yes/no): ",
+ "Continue execution? (yes/no): ",
+ ],
+ "test_cancelled": [
+ "测试程序已取消",
+ "Test program cancelled",
+ ],
+ # KuzuDB test environment setup
+ "warning_cleanup_temp_dir_failed": [
+ "警告: 清理临时目录失败: %s",
+ "Warning: Failed to cleanup temporary directory: %s",
+ ],
+ # Storage initialization errors
+ "error_general": [
+ "错误: %s",
+ "Error: %s",
+ ],
+ "error_module_path_not_found": [
+ "错误: 未找到 %s 的模块路径",
+ "Error: Module path not found for %s",
+ ],
+ "error_import_failed": [
+ "错误: 导入 %s 失败: %s",
+ "Error: Failed to import %s: %s",
+ ],
+ "error_initialization_failed": [
+ "错误: 初始化 %s 失败: %s",
+ "Error: Failed to initialize %s: %s",
+ ],
+ # New translations for storage setup
+ "supported_graph_storage_types": [
+ "支持的图存储类型: %s",
+ "Supported graph storage types: %s",
+ ],
+ "error_missing_env_vars": [
+ "错误: %s 需要以下环境变量,但未设置: %s",
+ "Error: %s requires these environment variables but they are not set: %s",
+ ],
+ "kuzu_test_environment_setup": [
+ "KuzuDB 测试环境已设置 | 测试环境已设置:\n%s",
+ "KuzuDB test environment setup | The test environment has been set up:\n%s",
+ ],
+}
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100755
index 0000000000..624f5cc10e
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,409 @@
+#!/usr/bin/env python3
+"""
+Interactive CLI for LightRAG Graph Storage Test Suite
+Allows users to select tests to run with bilingual support
+"""
+
+import asyncio
+import sys
+import os
+from typing import List
+import argparse
+
+# Add parent directory to path so we can import from tests
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from ascii_colors import ASCIIColors
+from tests.graph.core.storage_setup import (
+ initialize_graph_test_storage,
+ cleanup_kuzu_test_environment,
+)
+
+
+# Import test functions
+from tests.graph.tests.basic import test_graph_basic
+from tests.graph.tests.advanced import test_graph_advanced
+from tests.graph.tests.batch import test_graph_batch_operations
+from tests.graph.tests.special_chars import test_graph_special_characters
+from tests.graph.tests.undirected import test_graph_undirected_property
+
+
+class TestRunner:
+ """Interactive test runner with bilingual support"""
+
+ def __init__(self):
+ self.language = None # Will be set by language selection
+ self.test_functions = {} # Will be populated after language selection
+
+ def _initialize_test_functions(self):
+ """Initialize test functions after language is selected"""
+ self.test_functions = {
+ "basic": {
+ "func": test_graph_basic,
+ "name": self._get_text("basic_test_name"),
+ "description": self._get_text("basic_test_desc"),
+ },
+ "advanced": {
+ "func": test_graph_advanced,
+ "name": self._get_text("advanced_test_name"),
+ "description": self._get_text("advanced_test_desc"),
+ },
+ "batch": {
+ "func": test_graph_batch_operations,
+ "name": self._get_text("batch_test_name"),
+ "description": self._get_text("batch_test_desc"),
+ },
+ "special": {
+ "func": test_graph_special_characters,
+ "name": self._get_text("special_test_name"),
+ "description": self._get_text("special_test_desc"),
+ },
+ "undirected": {
+ "func": test_graph_undirected_property,
+ "name": self._get_text("undirected_test_name"),
+ "description": self._get_text("undirected_test_desc"),
+ },
+ }
+
+ def _get_text(self, key: str) -> str:
+ """Get translated text for a key"""
+ translations = {
+ "basic_test_name": ["基础测试", "Basic Test"],
+ "basic_test_desc": [
+ "节点插入、边创建、基本图操作",
+ "Node insertion, edge creation, basic graph operations",
+ ],
+ "advanced_test_name": ["高级测试", "Advanced Test"],
+ "advanced_test_desc": [
+ "复杂图结构、多跳关系、高级查询",
+ "Complex graph structures, multi-hop relationships, advanced queries",
+ ],
+ "batch_test_name": ["批量测试", "Batch Test"],
+ "batch_test_desc": [
+ "批量操作、事务处理、性能优化",
+ "Bulk operations, transaction handling, performance optimization",
+ ],
+ "special_test_name": ["特殊字符测试", "Special Characters Test"],
+ "special_test_desc": [
+ "Unicode支持、特殊字符编码、国际化",
+ "Unicode support, special character encoding, internationalization",
+ ],
+ "undirected_test_name": ["无向图测试", "Undirected Graph Test"],
+ "undirected_test_desc": [
+ "双向关系、无向图行为、一致性验证",
+ "Bidirectional relationships, undirected graph behavior, consistency validation",
+ ],
+ "welcome": [
+ "欢迎使用LightRAG图存储测试套件",
+ "Welcome to LightRAG Graph Storage Test Suite",
+ ],
+ "select_language": [
+ "请选择语言:",
+ "Please select language:",
+ ],
+ "select_tests": [
+ "请选择要运行的测试 (用逗号分隔多个选项,或输入 'all' 运行所有测试):",
+ "Please select tests to run (comma-separated for multiple, or 'all' for all tests):",
+ ],
+ "available_tests": ["可用测试:", "Available tests:"],
+ "invalid_selection": [
+ "无效选择,请重试",
+ "Invalid selection, please try again",
+ ],
+ "running_test": ["正在运行测试", "Running test"],
+ "test_passed": ["测试通过", "Test passed"],
+ "test_failed": ["测试失败", "Test failed"],
+ "all_tests_passed": ["所有测试通过!", "All tests passed!"],
+ "all_tests": ["所有测试", "All tests"],
+ "some_tests_failed": ["部分测试失败", "Some tests failed"],
+ "storage_init_failed": ["存储初始化失败", "Storage initialization failed"],
+ "test_summary": ["测试摘要", "Test Summary"],
+ "enter_choice": ["请输入选择", "Enter your choice"],
+ "press_enter": ["按回车键继续...", "Press Enter to continue..."],
+ "language_en": ["英语", "English"],
+ "language_zh": ["中文", "Chinese"],
+ "continue_question": [
+ "是否继续运行其他测试? (y/n)",
+ "Continue with other tests? (y/n)",
+ ],
+ "goodbye": ["再见!", "Goodbye!"],
+ "error_occurred": ["发生错误", "Error occurred"],
+ "select_storage": ["选择存储后端:", "Select storage backend:"],
+ "networkx_storage": ["NetworkX存储 (默认)", "NetworkX Storage (default)"],
+ "kuzu_storage": ["Kuzu数据库存储", "Kuzu Database Storage"],
+ "neo4j_storage": ["Neo4j存储", "Neo4j Storage"],
+ "mongodb_storage": ["MongoDB存储", "MongoDB Storage"],
+ "storage_selection": ["存储后端选择", "Storage Backend Selection"],
+ }
+
+ if key in translations:
+ return (
+ translations[key][0]
+ if self.language == "chinese"
+ else translations[key][1]
+ )
+ return key
+
+ def display_header(self):
+ """Display the program header"""
+ ASCIIColors.cyan("=" * 60)
+ ASCIIColors.yellow(self._get_text("welcome"))
+ ASCIIColors.cyan("=" * 60)
+ print()
+
+ def display_language_selection(self) -> str:
+ """Display language selection menu in both languages"""
+ print("=" * 60)
+ print("🌐 Language Selection / 语言选择")
+ print("=" * 60)
+ print("1. English")
+ print("2. 中文 (Chinese)")
+ print()
+
+ while True:
+ try:
+ choice = input("Enter your choice / 请输入选择 (1-2): ").strip()
+ if choice == "1":
+ return "english"
+ elif choice == "2":
+ return "chinese"
+ else:
+ print("❌ Invalid selection, please try again / 无效选择,请重试")
+ except KeyboardInterrupt:
+ print("\nGoodbye! / 再见!")
+ sys.exit(0)
+
+ def display_storage_selection(self) -> str:
+ """Display storage backend selection"""
+ ASCIIColors.blue(self._get_text("storage_selection"))
+ print(f"1. {self._get_text('networkx_storage')}")
+ print(f"2. {self._get_text('kuzu_storage')}")
+ print(f"3. {self._get_text('neo4j_storage')}")
+ print(f"4. {self._get_text('mongodb_storage')}")
+ print()
+
+ while True:
+ try:
+ choice = input(f"{self._get_text('enter_choice')} (1-4): ").strip()
+ if choice == "1":
+ return "NetworkXStorage"
+ elif choice == "2":
+ return "KuzuDBStorage"
+ elif choice == "3":
+ return "Neo4JStorage"
+ elif choice == "4":
+ return "MongoGraphStorage"
+ else:
+ ASCIIColors.red(self._get_text("invalid_selection"))
+ except KeyboardInterrupt:
+ print(f"\n{self._get_text('goodbye')}")
+ sys.exit(0)
+
+ def display_test_menu(self) -> List[str]:
+ """Display test selection menu and get user choice"""
+ ASCIIColors.blue(self._get_text("available_tests"))
+ print()
+
+ for i, (key, test_info) in enumerate(self.test_functions.items(), 1):
+ ASCIIColors.green(f"{i}. {test_info['name']}")
+ print(f" {test_info['description']}")
+ print()
+
+ ASCIIColors.yellow(
+ f"{len(self.test_functions) + 1}. {self._get_text('all_tests')}"
+ )
+ print()
+
+ while True:
+ try:
+ ASCIIColors.blue(self._get_text("select_tests"))
+ choice = input(f"{self._get_text('enter_choice')}: ").strip()
+
+ if choice.lower() == "all":
+ return list(self.test_functions.keys())
+
+ # Parse comma-separated choices
+ selected_tests = []
+ for c in choice.split(","):
+ c = c.strip()
+ if c.isdigit():
+ idx = int(c) - 1
+ if 0 <= idx < len(self.test_functions):
+ test_key = list(self.test_functions.keys())[idx]
+ selected_tests.append(test_key)
+ elif idx == len(self.test_functions): # "All tests" option
+ return list(self.test_functions.keys())
+ else:
+ # Try to match test name directly
+ if c in self.test_functions:
+ selected_tests.append(c)
+
+ if selected_tests:
+ return selected_tests
+ else:
+ ASCIIColors.red(self._get_text("invalid_selection"))
+
+ except KeyboardInterrupt:
+ print(f"\n{self._get_text('goodbye')}")
+ sys.exit(0)
+
+ async def run_test(self, test_key: str, storage) -> bool:
+ """Run a specific test"""
+ if test_key not in self.test_functions:
+ return False
+
+ test_info = self.test_functions[test_key]
+ ASCIIColors.blue(f"\n{self._get_text('running_test')}: {test_info['name']}")
+ ASCIIColors.cyan("=" * 50)
+
+ try:
+ result = await test_info["func"](storage)
+ if result:
+ ASCIIColors.green(
+ f"✅ {test_info['name']} - {self._get_text('test_passed')}"
+ )
+ else:
+ ASCIIColors.red(
+ f"❌ {test_info['name']} - {self._get_text('test_failed')}"
+ )
+ return result
+ except Exception as e:
+ ASCIIColors.red(
+ f"❌ {test_info['name']} - {self._get_text('error_occurred')}: {e}"
+ )
+ return False
+
+ async def run_selected_tests(self, selected_tests: List[str], storage_backend: str):
+ """Run the selected tests"""
+ # Set storage backend
+ os.environ["LIGHTRAG_GRAPH_STORAGE"] = storage_backend
+
+ # Initialize storage
+ storage = await initialize_graph_test_storage()
+ if storage is None:
+ ASCIIColors.red(self._get_text("storage_init_failed"))
+ return
+
+ try:
+ results = {}
+ for test_key in selected_tests:
+ success = await self.run_test(test_key, storage)
+ results[test_key] = success
+
+ # Display summary
+ print("\n" + "=" * 60)
+ ASCIIColors.yellow(self._get_text("test_summary"))
+ print("=" * 60)
+
+ passed = sum(1 for success in results.values() if success)
+ total = len(results)
+
+ for test_key, success in results.items():
+ test_name = self.test_functions[test_key]["name"]
+ status = (
+ self._get_text("test_passed")
+ if success
+ else self._get_text("test_failed")
+ )
+ color = ASCIIColors.green if success else ASCIIColors.red
+ color(f"{'✅' if success else '❌'} {test_name}: {status}")
+
+ print(f"\n{self._get_text('test_summary')}: {passed}/{total}")
+
+ if passed == total:
+ ASCIIColors.green(self._get_text("all_tests_passed"))
+ else:
+ ASCIIColors.red(self._get_text("some_tests_failed"))
+
+ finally:
+ # Cleanup
+ if storage and hasattr(storage, "close"):
+ await storage.close()
+ if storage and hasattr(storage, "_temp_dir"):
+ cleanup_kuzu_test_environment(storage._temp_dir)
+
+ async def run_interactive(self):
+ """Run the interactive test selection"""
+ # Language selection (always shown)
+ self.language = self.display_language_selection()
+ os.environ["TEST_LANGUAGE"] = self.language
+
+ # Initialize test functions after language is selected
+ self._initialize_test_functions()
+
+ # Display header
+ self.display_header()
+
+ # Storage backend selection
+ storage_backend = self.display_storage_selection()
+
+ while True:
+ try:
+ # Test selection
+ selected_tests = self.display_test_menu()
+
+ # Run tests
+ await self.run_selected_tests(selected_tests, storage_backend)
+
+ # Ask if user wants to continue
+ print(f"\n{self._get_text('continue_question')}")
+ continue_choice = input().strip().lower()
+
+ if continue_choice not in ["y", "yes", "是", "是的"]:
+ break
+
+ except KeyboardInterrupt:
+ print(f"\n{self._get_text('goodbye')}")
+ break
+
+ print(f"\n{self._get_text('goodbye')}")
+
+
+def main():
+ """Main CLI entry point"""
+ parser = argparse.ArgumentParser(
+ description="Interactive LightRAG Graph Storage Test Suite"
+ )
+ parser.add_argument(
+ "--language",
+ choices=["english", "chinese"],
+ help="Language for the interface (if not specified, user will be prompted)",
+ )
+ parser.add_argument(
+ "--storage",
+ choices=[
+ "NetworkXStorage",
+ "KuzuDBStorage",
+ "Neo4JStorage",
+ "MongoGraphStorage",
+ ],
+ help="Storage backend to use (if not specified, user will be prompted)",
+ )
+ parser.add_argument(
+ "--tests",
+ nargs="+",
+ choices=["basic", "advanced", "batch", "special", "undirected", "all"],
+ help="Tests to run (if not specified, user will be prompted)",
+ )
+
+ args = parser.parse_args()
+
+ # Quick mode - if all parameters are provided, run without interaction
+ if args.language and args.storage and args.tests:
+ runner = TestRunner()
+ runner.language = args.language
+ os.environ["TEST_LANGUAGE"] = args.language
+ runner._initialize_test_functions()
+
+ selected_tests = (
+ list(runner.test_functions.keys()) if "all" in args.tests else args.tests
+ )
+ asyncio.run(runner.run_selected_tests(selected_tests, args.storage))
+ else:
+ # Interactive mode
+ runner = TestRunner()
+ asyncio.run(runner.run_interactive())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/test_graph_storage.py b/tests/test_graph_storage.py
index 62f658ff72..c3ec8b4fa9 100644
--- a/tests/test_graph_storage.py
+++ b/tests/test_graph_storage.py
@@ -1,1256 +1,88 @@
-#!/usr/bin/env python
"""
-通用图存储测试程序
-
-该程序根据.env中的LIGHTRAG_GRAPH_STORAGE配置选择使用的图存储类型,
-并对其进行基本操作和高级操作的测试。
-
-支持的图存储类型包括:
-- NetworkXStorage
-- Neo4JStorage
-- MongoDBStorage
-- PGGraphStorage
-- MemgraphStorage
+Pytest integration for the modular graph storage test suite
"""
-import asyncio
+import pytest
import os
-import sys
-import importlib
-import numpy as np
-from dotenv import load_dotenv
-from ascii_colors import ASCIIColors
-
-# 添加项目根目录到Python路径
-sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-from lightrag.types import KnowledgeGraph
-from lightrag.kg import (
- STORAGE_IMPLEMENTATIONS,
- STORAGE_ENV_REQUIREMENTS,
- STORAGES,
- verify_storage_implementation,
+from tests.graph.core.storage_setup import (
+ initialize_graph_test_storage,
+ cleanup_kuzu_test_environment,
)
-from lightrag.kg.shared_storage import initialize_share_data
-from lightrag.constants import GRAPH_FIELD_SEP
-
-
-# 模拟的嵌入函数,返回随机向量
-async def mock_embedding_func(texts):
- return np.random.rand(len(texts), 10) # 返回10维随机向量
-
-
-def check_env_file():
- """
- 检查.env文件是否存在,如果不存在则发出警告
- 返回True表示应该继续执行,False表示应该退出
- """
- if not os.path.exists(".env"):
- warning_msg = "警告: 当前目录中没有找到.env文件,这可能会影响存储配置的加载。"
- ASCIIColors.yellow(warning_msg)
-
- # 检查是否在交互式终端中运行
- if sys.stdin.isatty():
- response = input("是否继续执行? (yes/no): ")
- if response.lower() != "yes":
- ASCIIColors.red("测试程序已取消")
- return False
- return True
-
-
-async def initialize_graph_storage():
- """
- 根据环境变量初始化相应的图存储实例
- 返回初始化的存储实例
- """
- # 从环境变量中获取图存储类型
- graph_storage_type = os.getenv("LIGHTRAG_GRAPH_STORAGE", "NetworkXStorage")
-
- # 验证存储类型是否有效
- try:
- verify_storage_implementation("GRAPH_STORAGE", graph_storage_type)
- except ValueError as e:
- ASCIIColors.red(f"错误: {str(e)}")
- ASCIIColors.yellow(
- f"支持的图存储类型: {', '.join(STORAGE_IMPLEMENTATIONS['GRAPH_STORAGE']['implementations'])}"
- )
- return None
-
- # 检查所需的环境变量
- required_env_vars = STORAGE_ENV_REQUIREMENTS.get(graph_storage_type, [])
- missing_env_vars = [var for var in required_env_vars if not os.getenv(var)]
-
- if missing_env_vars:
- ASCIIColors.red(
- f"错误: {graph_storage_type} 需要以下环境变量,但未设置: {', '.join(missing_env_vars)}"
- )
- return None
-
- # 动态导入相应的模块
- module_path = STORAGES.get(graph_storage_type)
- if not module_path:
- ASCIIColors.red(f"错误: 未找到 {graph_storage_type} 的模块路径")
- return None
-
- try:
- module = importlib.import_module(module_path, package="lightrag")
- storage_class = getattr(module, graph_storage_type)
- except (ImportError, AttributeError) as e:
- ASCIIColors.red(f"错误: 导入 {graph_storage_type} 失败: {str(e)}")
- return None
-
- # 初始化存储实例
- global_config = {
- "embedding_batch_num": 10, # 批处理大小
- "vector_db_storage_cls_kwargs": {
- "cosine_better_than_threshold": 0.5 # 余弦相似度阈值
- },
- "working_dir": os.environ.get("WORKING_DIR", "./rag_storage"), # 工作目录
- }
-
- # 如果使用 NetworkXStorage,需要先初始化 shared_storage
- if graph_storage_type == "NetworkXStorage":
- initialize_share_data() # 使用单进程模式
-
- try:
- storage = storage_class(
- namespace="test_graph",
- global_config=global_config,
- embedding_func=mock_embedding_func,
- )
-
- # 初始化连接
- await storage.initialize()
- return storage
- except Exception as e:
- ASCIIColors.red(f"错误: 初始化 {graph_storage_type} 失败: {str(e)}")
- return None
-
-
-async def test_graph_basic(storage):
- """
- 测试图数据库的基本操作:
- 1. 使用 upsert_node 插入两个节点
- 2. 使用 upsert_edge 插入一条连接两个节点的边
- 3. 使用 get_node 读取一个节点
- 4. 使用 get_edge 读取一条边
- """
- try:
- # 1. 插入第一个节点
- node1_id = "人工智能"
- node1_data = {
- "entity_id": node1_id,
- "description": "人工智能是计算机科学的一个分支,它企图了解智能的实质,并生产出一种新的能以人类智能相似的方式做出反应的智能机器。",
- "keywords": "AI,机器学习,深度学习",
- "entity_type": "技术领域",
- }
- print(f"插入节点1: {node1_id}")
- await storage.upsert_node(node1_id, node1_data)
-
- # 2. 插入第二个节点
- node2_id = "机器学习"
- node2_data = {
- "entity_id": node2_id,
- "description": "机器学习是人工智能的一个分支,它使用统计学方法让计算机系统在不被明确编程的情况下也能够学习。",
- "keywords": "监督学习,无监督学习,强化学习",
- "entity_type": "技术领域",
- }
- print(f"插入节点2: {node2_id}")
- await storage.upsert_node(node2_id, node2_data)
-
- # 3. 插入连接边
- edge_data = {
- "relationship": "包含",
- "weight": 1.0,
- "description": "人工智能领域包含机器学习这个子领域",
- }
- print(f"插入边: {node1_id} -> {node2_id}")
- await storage.upsert_edge(node1_id, node2_id, edge_data)
-
- # 4. 读取节点属性
- print(f"读取节点属性: {node1_id}")
- node1_props = await storage.get_node(node1_id)
- if node1_props:
- print(f"成功读取节点属性: {node1_id}")
- print(f"节点描述: {node1_props.get('description', '无描述')}")
- print(f"节点类型: {node1_props.get('entity_type', '无类型')}")
- print(f"节点关键词: {node1_props.get('keywords', '无关键词')}")
- # 验证返回的属性是否正确
- assert (
- node1_props.get("entity_id") == node1_id
- ), f"节点ID不匹配: 期望 {node1_id}, 实际 {node1_props.get('entity_id')}"
- assert (
- node1_props.get("description") == node1_data["description"]
- ), "节点描述不匹配"
- assert (
- node1_props.get("entity_type") == node1_data["entity_type"]
- ), "节点类型不匹配"
- else:
- print(f"读取节点属性失败: {node1_id}")
- assert False, f"未能读取节点属性: {node1_id}"
-
- # 5. 读取边属性
- print(f"读取边属性: {node1_id} -> {node2_id}")
- edge_props = await storage.get_edge(node1_id, node2_id)
- if edge_props:
- print(f"成功读取边属性: {node1_id} -> {node2_id}")
- print(f"边关系: {edge_props.get('relationship', '无关系')}")
- print(f"边描述: {edge_props.get('description', '无描述')}")
- print(f"边权重: {edge_props.get('weight', '无权重')}")
- # 验证返回的属性是否正确
- assert (
- edge_props.get("relationship") == edge_data["relationship"]
- ), "边关系不匹配"
- assert (
- edge_props.get("description") == edge_data["description"]
- ), "边描述不匹配"
- assert edge_props.get("weight") == edge_data["weight"], "边权重不匹配"
- else:
- print(f"读取边属性失败: {node1_id} -> {node2_id}")
- assert False, f"未能读取边属性: {node1_id} -> {node2_id}"
-
- # 5.1 验证无向图特性 - 读取反向边属性
- print(f"读取反向边属性: {node2_id} -> {node1_id}")
- reverse_edge_props = await storage.get_edge(node2_id, node1_id)
- if reverse_edge_props:
- print(f"成功读取反向边属性: {node2_id} -> {node1_id}")
- print(f"反向边关系: {reverse_edge_props.get('relationship', '无关系')}")
- print(f"反向边描述: {reverse_edge_props.get('description', '无描述')}")
- print(f"反向边权重: {reverse_edge_props.get('weight', '无权重')}")
- # 验证正向和反向边属性是否相同
- assert (
- edge_props == reverse_edge_props
- ), "正向和反向边属性不一致,无向图特性验证失败"
- print("无向图特性验证成功:正向和反向边属性一致")
- else:
- print(f"读取反向边属性失败: {node2_id} -> {node1_id}")
- assert (
- False
- ), f"未能读取反向边属性: {node2_id} -> {node1_id},无向图特性验证失败"
-
- print("基本测试完成,数据已保留在数据库中")
- return True
-
- except Exception as e:
- ASCIIColors.red(f"测试过程中发生错误: {str(e)}")
- return False
-
-
-async def test_graph_advanced(storage):
- """
- 测试图数据库的高级操作:
- 1. 使用 node_degree 获取节点的度数
- 2. 使用 edge_degree 获取边的度数
- 3. 使用 get_node_edges 获取节点的所有边
- 4. 使用 get_all_labels 获取所有标签
- 5. 使用 get_knowledge_graph 获取知识图谱
- 6. 使用 delete_node 删除节点
- 7. 使用 remove_nodes 批量删除节点
- 8. 使用 remove_edges 删除边
- 9. 使用 drop 清理数据
- """
- try:
- # 1. 插入测试数据
- # 插入节点1: 人工智能
- node1_id = "人工智能"
- node1_data = {
- "entity_id": node1_id,
- "description": "人工智能是计算机科学的一个分支,它企图了解智能的实质,并生产出一种新的能以人类智能相似的方式做出反应的智能机器。",
- "keywords": "AI,机器学习,深度学习",
- "entity_type": "技术领域",
- }
- print(f"插入节点1: {node1_id}")
- await storage.upsert_node(node1_id, node1_data)
-
- # 插入节点2: 机器学习
- node2_id = "机器学习"
- node2_data = {
- "entity_id": node2_id,
- "description": "机器学习是人工智能的一个分支,它使用统计学方法让计算机系统在不被明确编程的情况下也能够学习。",
- "keywords": "监督学习,无监督学习,强化学习",
- "entity_type": "技术领域",
- }
- print(f"插入节点2: {node2_id}")
- await storage.upsert_node(node2_id, node2_data)
-
- # 插入节点3: 深度学习
- node3_id = "深度学习"
- node3_data = {
- "entity_id": node3_id,
- "description": "深度学习是机器学习的一个分支,它使用多层神经网络来模拟人脑的学习过程。",
- "keywords": "神经网络,CNN,RNN",
- "entity_type": "技术领域",
- }
- print(f"插入节点3: {node3_id}")
- await storage.upsert_node(node3_id, node3_data)
-
- # 插入边1: 人工智能 -> 机器学习
- edge1_data = {
- "relationship": "包含",
- "weight": 1.0,
- "description": "人工智能领域包含机器学习这个子领域",
- }
- print(f"插入边1: {node1_id} -> {node2_id}")
- await storage.upsert_edge(node1_id, node2_id, edge1_data)
-
- # 插入边2: 机器学习 -> 深度学习
- edge2_data = {
- "relationship": "包含",
- "weight": 1.0,
- "description": "机器学习领域包含深度学习这个子领域",
- }
- print(f"插入边2: {node2_id} -> {node3_id}")
- await storage.upsert_edge(node2_id, node3_id, edge2_data)
-
- # 2. 测试 node_degree - 获取节点的度数
- print(f"== 测试 node_degree: {node1_id}")
- node1_degree = await storage.node_degree(node1_id)
- print(f"节点 {node1_id} 的度数: {node1_degree}")
- assert node1_degree == 1, f"节点 {node1_id} 的度数应为1,实际为 {node1_degree}"
-
- # 2.1 测试所有节点的度数
- print("== 测试所有节点的度数")
- node2_degree = await storage.node_degree(node2_id)
- node3_degree = await storage.node_degree(node3_id)
- print(f"节点 {node2_id} 的度数: {node2_degree}")
- print(f"节点 {node3_id} 的度数: {node3_degree}")
- assert node2_degree == 2, f"节点 {node2_id} 的度数应为2,实际为 {node2_degree}"
- assert node3_degree == 1, f"节点 {node3_id} 的度数应为1,实际为 {node3_degree}"
-
- # 3. 测试 edge_degree - 获取边的度数
- print(f"== 测试 edge_degree: {node1_id} -> {node2_id}")
- edge_degree = await storage.edge_degree(node1_id, node2_id)
- print(f"边 {node1_id} -> {node2_id} 的度数: {edge_degree}")
- assert (
- edge_degree == 3
- ), f"边 {node1_id} -> {node2_id} 的度数应为3,实际为 {edge_degree}"
-
- # 3.1 测试反向边的度数 - 验证无向图特性
- print(f"== 测试反向边的度数: {node2_id} -> {node1_id}")
- reverse_edge_degree = await storage.edge_degree(node2_id, node1_id)
- print(f"反向边 {node2_id} -> {node1_id} 的度数: {reverse_edge_degree}")
- assert (
- edge_degree == reverse_edge_degree
- ), "正向边和反向边的度数不一致,无向图特性验证失败"
- print("无向图特性验证成功:正向边和反向边的度数一致")
-
- # 4. 测试 get_node_edges - 获取节点的所有边
- print(f"== 测试 get_node_edges: {node2_id}")
- node2_edges = await storage.get_node_edges(node2_id)
- print(f"节点 {node2_id} 的所有边: {node2_edges}")
- assert (
- len(node2_edges) == 2
- ), f"节点 {node2_id} 应有2条边,实际有 {len(node2_edges)}"
-
- # 4.1 验证节点边的无向图特性
- print("== 验证节点边的无向图特性")
- # 检查是否包含与node1和node3的连接关系(无论方向)
- has_connection_with_node1 = False
- has_connection_with_node3 = False
- for edge in node2_edges:
- # 检查是否有与node1的连接(无论方向)
- if (edge[0] == node1_id and edge[1] == node2_id) or (
- edge[0] == node2_id and edge[1] == node1_id
- ):
- has_connection_with_node1 = True
- # 检查是否有与node3的连接(无论方向)
- if (edge[0] == node2_id and edge[1] == node3_id) or (
- edge[0] == node3_id and edge[1] == node2_id
- ):
- has_connection_with_node3 = True
-
- assert (
- has_connection_with_node1
- ), f"节点 {node2_id} 的边列表中应包含与 {node1_id} 的连接"
- assert (
- has_connection_with_node3
- ), f"节点 {node2_id} 的边列表中应包含与 {node3_id} 的连接"
- print(f"无向图特性验证成功:节点 {node2_id} 的边列表包含所有相关的边")
-
- # 5. 测试 get_all_labels - 获取所有标签
- print("== 测试 get_all_labels")
- all_labels = await storage.get_all_labels()
- print(f"所有标签: {all_labels}")
- assert len(all_labels) == 3, f"应有3个标签,实际有 {len(all_labels)}"
- assert node1_id in all_labels, f"{node1_id} 应在标签列表中"
- assert node2_id in all_labels, f"{node2_id} 应在标签列表中"
- assert node3_id in all_labels, f"{node3_id} 应在标签列表中"
-
- # 6. 测试 get_knowledge_graph - 获取知识图谱
- print("== 测试 get_knowledge_graph")
- kg = await storage.get_knowledge_graph("*", max_depth=2, max_nodes=10)
- print(f"知识图谱节点数: {len(kg.nodes)}")
- print(f"知识图谱边数: {len(kg.edges)}")
- assert isinstance(kg, KnowledgeGraph), "返回结果应为 KnowledgeGraph 类型"
- assert len(kg.nodes) == 3, f"知识图谱应有3个节点,实际有 {len(kg.nodes)}"
- assert len(kg.edges) == 2, f"知识图谱应有2条边,实际有 {len(kg.edges)}"
-
- # 7. 测试 delete_node - 删除节点
- print(f"== 测试 delete_node: {node3_id}")
- await storage.delete_node(node3_id)
- node3_props = await storage.get_node(node3_id)
- print(f"删除后查询节点属性 {node3_id}: {node3_props}")
- assert node3_props is None, f"节点 {node3_id} 应已被删除"
-
- # 重新插入节点3用于后续测试
- await storage.upsert_node(node3_id, node3_data)
- await storage.upsert_edge(node2_id, node3_id, edge2_data)
-
- # 8. 测试 remove_edges - 删除边
- print(f"== 测试 remove_edges: {node2_id} -> {node3_id}")
- await storage.remove_edges([(node2_id, node3_id)])
- edge_props = await storage.get_edge(node2_id, node3_id)
- print(f"删除后查询边属性 {node2_id} -> {node3_id}: {edge_props}")
- assert edge_props is None, f"边 {node2_id} -> {node3_id} 应已被删除"
-
- # 8.1 验证删除边的无向图特性
- print(f"== 验证删除边的无向图特性: {node3_id} -> {node2_id}")
- reverse_edge_props = await storage.get_edge(node3_id, node2_id)
- print(f"删除后查询反向边属性 {node3_id} -> {node2_id}: {reverse_edge_props}")
- assert (
- reverse_edge_props is None
- ), f"反向边 {node3_id} -> {node2_id} 也应被删除,无向图特性验证失败"
- print("无向图特性验证成功:删除一个方向的边后,反向边也被删除")
-
- # 9. 测试 remove_nodes - 批量删除节点
- print(f"== 测试 remove_nodes: [{node2_id}, {node3_id}]")
- await storage.remove_nodes([node2_id, node3_id])
- node2_props = await storage.get_node(node2_id)
- node3_props = await storage.get_node(node3_id)
- print(f"删除后查询节点属性 {node2_id}: {node2_props}")
- print(f"删除后查询节点属性 {node3_id}: {node3_props}")
- assert node2_props is None, f"节点 {node2_id} 应已被删除"
- assert node3_props is None, f"节点 {node3_id} 应已被删除"
-
- print("\n高级测试完成")
- return True
-
- except Exception as e:
- ASCIIColors.red(f"测试过程中发生错误: {str(e)}")
- return False
-
-
-async def test_graph_batch_operations(storage):
- """
- 测试图数据库的批量操作:
- 1. 使用 get_nodes_batch 批量获取多个节点的属性
- 2. 使用 node_degrees_batch 批量获取多个节点的度数
- 3. 使用 edge_degrees_batch 批量获取多个边的度数
- 4. 使用 get_edges_batch 批量获取多个边的属性
- 5. 使用 get_nodes_edges_batch 批量获取多个节点的所有边
- """
- try:
- chunk1_id = "1"
- chunk2_id = "2"
- chunk3_id = "3"
- # 1. 插入测试数据
- # 插入节点1: 人工智能
- node1_id = "人工智能"
- node1_data = {
- "entity_id": node1_id,
- "description": "人工智能是计算机科学的一个分支,它企图了解智能的实质,并生产出一种新的能以人类智能相似的方式做出反应的智能机器。",
- "keywords": "AI,机器学习,深度学习",
- "entity_type": "技术领域",
- "source_id": GRAPH_FIELD_SEP.join([chunk1_id, chunk2_id]),
- }
- print(f"插入节点1: {node1_id}")
- await storage.upsert_node(node1_id, node1_data)
-
- # 插入节点2: 机器学习
- node2_id = "机器学习"
- node2_data = {
- "entity_id": node2_id,
- "description": "机器学习是人工智能的一个分支,它使用统计学方法让计算机系统在不被明确编程的情况下也能够学习。",
- "keywords": "监督学习,无监督学习,强化学习",
- "entity_type": "技术领域",
- "source_id": GRAPH_FIELD_SEP.join([chunk2_id, chunk3_id]),
- }
- print(f"插入节点2: {node2_id}")
- await storage.upsert_node(node2_id, node2_data)
-
- # 插入节点3: 深度学习
- node3_id = "深度学习"
- node3_data = {
- "entity_id": node3_id,
- "description": "深度学习是机器学习的一个分支,它使用多层神经网络来模拟人脑的学习过程。",
- "keywords": "神经网络,CNN,RNN",
- "entity_type": "技术领域",
- "source_id": GRAPH_FIELD_SEP.join([chunk3_id]),
- }
- print(f"插入节点3: {node3_id}")
- await storage.upsert_node(node3_id, node3_data)
-
- # 插入节点4: 自然语言处理
- node4_id = "自然语言处理"
- node4_data = {
- "entity_id": node4_id,
- "description": "自然语言处理是人工智能的一个分支,专注于使计算机理解和处理人类语言。",
- "keywords": "NLP,文本分析,语言模型",
- "entity_type": "技术领域",
- }
- print(f"插入节点4: {node4_id}")
- await storage.upsert_node(node4_id, node4_data)
-
- # 插入节点5: 计算机视觉
- node5_id = "计算机视觉"
- node5_data = {
- "entity_id": node5_id,
- "description": "计算机视觉是人工智能的一个分支,专注于使计算机能够从图像或视频中获取信息。",
- "keywords": "CV,图像识别,目标检测",
- "entity_type": "技术领域",
- }
- print(f"插入节点5: {node5_id}")
- await storage.upsert_node(node5_id, node5_data)
-
- # 插入边1: 人工智能 -> 机器学习
- edge1_data = {
- "relationship": "包含",
- "weight": 1.0,
- "description": "人工智能领域包含机器学习这个子领域",
- "source_id": GRAPH_FIELD_SEP.join([chunk1_id, chunk2_id]),
- }
- print(f"插入边1: {node1_id} -> {node2_id}")
- await storage.upsert_edge(node1_id, node2_id, edge1_data)
-
- # 插入边2: 机器学习 -> 深度学习
- edge2_data = {
- "relationship": "包含",
- "weight": 1.0,
- "description": "机器学习领域包含深度学习这个子领域",
- "source_id": GRAPH_FIELD_SEP.join([chunk2_id, chunk3_id]),
- }
- print(f"插入边2: {node2_id} -> {node3_id}")
- await storage.upsert_edge(node2_id, node3_id, edge2_data)
-
- # 插入边3: 人工智能 -> 自然语言处理
- edge3_data = {
- "relationship": "包含",
- "weight": 1.0,
- "description": "人工智能领域包含自然语言处理这个子领域",
- "source_id": GRAPH_FIELD_SEP.join([chunk3_id]),
- }
- print(f"插入边3: {node1_id} -> {node4_id}")
- await storage.upsert_edge(node1_id, node4_id, edge3_data)
-
- # 插入边4: 人工智能 -> 计算机视觉
- edge4_data = {
- "relationship": "包含",
- "weight": 1.0,
- "description": "人工智能领域包含计算机视觉这个子领域",
- }
- print(f"插入边4: {node1_id} -> {node5_id}")
- await storage.upsert_edge(node1_id, node5_id, edge4_data)
-
- # 插入边5: 深度学习 -> 自然语言处理
- edge5_data = {
- "relationship": "应用于",
- "weight": 0.8,
- "description": "深度学习技术应用于自然语言处理领域",
- }
- print(f"插入边5: {node3_id} -> {node4_id}")
- await storage.upsert_edge(node3_id, node4_id, edge5_data)
-
- # 插入边6: 深度学习 -> 计算机视觉
- edge6_data = {
- "relationship": "应用于",
- "weight": 0.8,
- "description": "深度学习技术应用于计算机视觉领域",
- }
- print(f"插入边6: {node3_id} -> {node5_id}")
- await storage.upsert_edge(node3_id, node5_id, edge6_data)
-
- # 2. 测试 get_nodes_batch - 批量获取多个节点的属性
- print("== 测试 get_nodes_batch")
- node_ids = [node1_id, node2_id, node3_id]
- nodes_dict = await storage.get_nodes_batch(node_ids)
- print(f"批量获取节点属性结果: {nodes_dict.keys()}")
- assert len(nodes_dict) == 3, f"应返回3个节点,实际返回 {len(nodes_dict)} 个"
- assert node1_id in nodes_dict, f"{node1_id} 应在返回结果中"
- assert node2_id in nodes_dict, f"{node2_id} 应在返回结果中"
- assert node3_id in nodes_dict, f"{node3_id} 应在返回结果中"
- assert (
- nodes_dict[node1_id]["description"] == node1_data["description"]
- ), f"{node1_id} 描述不匹配"
- assert (
- nodes_dict[node2_id]["description"] == node2_data["description"]
- ), f"{node2_id} 描述不匹配"
- assert (
- nodes_dict[node3_id]["description"] == node3_data["description"]
- ), f"{node3_id} 描述不匹配"
-
- # 3. 测试 node_degrees_batch - 批量获取多个节点的度数
- print("== 测试 node_degrees_batch")
- node_degrees = await storage.node_degrees_batch(node_ids)
- print(f"批量获取节点度数结果: {node_degrees}")
- assert (
- len(node_degrees) == 3
- ), f"应返回3个节点的度数,实际返回 {len(node_degrees)} 个"
- assert node1_id in node_degrees, f"{node1_id} 应在返回结果中"
- assert node2_id in node_degrees, f"{node2_id} 应在返回结果中"
- assert node3_id in node_degrees, f"{node3_id} 应在返回结果中"
- assert (
- node_degrees[node1_id] == 3
- ), f"{node1_id} 度数应为3,实际为 {node_degrees[node1_id]}"
- assert (
- node_degrees[node2_id] == 2
- ), f"{node2_id} 度数应为2,实际为 {node_degrees[node2_id]}"
- assert (
- node_degrees[node3_id] == 3
- ), f"{node3_id} 度数应为3,实际为 {node_degrees[node3_id]}"
-
- # 4. 测试 edge_degrees_batch - 批量获取多个边的度数
- print("== 测试 edge_degrees_batch")
- edges = [(node1_id, node2_id), (node2_id, node3_id), (node3_id, node4_id)]
- edge_degrees = await storage.edge_degrees_batch(edges)
- print(f"批量获取边度数结果: {edge_degrees}")
- assert (
- len(edge_degrees) == 3
- ), f"应返回3条边的度数,实际返回 {len(edge_degrees)} 条"
- assert (
- node1_id,
- node2_id,
- ) in edge_degrees, f"边 {node1_id} -> {node2_id} 应在返回结果中"
- assert (
- node2_id,
- node3_id,
- ) in edge_degrees, f"边 {node2_id} -> {node3_id} 应在返回结果中"
- assert (
- node3_id,
- node4_id,
- ) in edge_degrees, f"边 {node3_id} -> {node4_id} 应在返回结果中"
- # 验证边的度数是否正确(源节点度数 + 目标节点度数)
- assert (
- edge_degrees[(node1_id, node2_id)] == 5
- ), f"边 {node1_id} -> {node2_id} 度数应为5,实际为 {edge_degrees[(node1_id, node2_id)]}"
- assert (
- edge_degrees[(node2_id, node3_id)] == 5
- ), f"边 {node2_id} -> {node3_id} 度数应为5,实际为 {edge_degrees[(node2_id, node3_id)]}"
- assert (
- edge_degrees[(node3_id, node4_id)] == 5
- ), f"边 {node3_id} -> {node4_id} 度数应为5,实际为 {edge_degrees[(node3_id, node4_id)]}"
-
- # 5. 测试 get_edges_batch - 批量获取多个边的属性
- print("== 测试 get_edges_batch")
- # 将元组列表转换为Neo4j风格的字典列表
- edge_dicts = [{"src": src, "tgt": tgt} for src, tgt in edges]
- edges_dict = await storage.get_edges_batch(edge_dicts)
- print(f"批量获取边属性结果: {edges_dict.keys()}")
- assert len(edges_dict) == 3, f"应返回3条边的属性,实际返回 {len(edges_dict)} 条"
- assert (
- node1_id,
- node2_id,
- ) in edges_dict, f"边 {node1_id} -> {node2_id} 应在返回结果中"
- assert (
- node2_id,
- node3_id,
- ) in edges_dict, f"边 {node2_id} -> {node3_id} 应在返回结果中"
- assert (
- node3_id,
- node4_id,
- ) in edges_dict, f"边 {node3_id} -> {node4_id} 应在返回结果中"
- assert (
- edges_dict[(node1_id, node2_id)]["relationship"]
- == edge1_data["relationship"]
- ), f"边 {node1_id} -> {node2_id} 关系不匹配"
- assert (
- edges_dict[(node2_id, node3_id)]["relationship"]
- == edge2_data["relationship"]
- ), f"边 {node2_id} -> {node3_id} 关系不匹配"
- assert (
- edges_dict[(node3_id, node4_id)]["relationship"]
- == edge5_data["relationship"]
- ), f"边 {node3_id} -> {node4_id} 关系不匹配"
-
- # 5.1 测试反向边的批量获取 - 验证无向图特性
- print("== 测试反向边的批量获取")
- # 创建反向边的字典列表
- reverse_edge_dicts = [{"src": tgt, "tgt": src} for src, tgt in edges]
- reverse_edges_dict = await storage.get_edges_batch(reverse_edge_dicts)
- print(f"批量获取反向边属性结果: {reverse_edges_dict.keys()}")
- assert (
- len(reverse_edges_dict) == 3
- ), f"应返回3条反向边的属性,实际返回 {len(reverse_edges_dict)} 条"
-
- # 验证正向和反向边的属性是否一致
- for (src, tgt), props in edges_dict.items():
- assert (
- tgt,
- src,
- ) in reverse_edges_dict, f"反向边 {tgt} -> {src} 应在返回结果中"
- assert (
- props == reverse_edges_dict[(tgt, src)]
- ), f"边 {src} -> {tgt} 和反向边 {tgt} -> {src} 的属性不一致"
-
- print("无向图特性验证成功:批量获取的正向和反向边属性一致")
-
- # 6. 测试 get_nodes_edges_batch - 批量获取多个节点的所有边
- print("== 测试 get_nodes_edges_batch")
- nodes_edges = await storage.get_nodes_edges_batch([node1_id, node3_id])
- print(f"批量获取节点边结果: {nodes_edges.keys()}")
- assert (
- len(nodes_edges) == 2
- ), f"应返回2个节点的边,实际返回 {len(nodes_edges)} 个"
- assert node1_id in nodes_edges, f"{node1_id} 应在返回结果中"
- assert node3_id in nodes_edges, f"{node3_id} 应在返回结果中"
- assert (
- len(nodes_edges[node1_id]) == 3
- ), f"{node1_id} 应有3条边,实际有 {len(nodes_edges[node1_id])} 条"
- assert (
- len(nodes_edges[node3_id]) == 3
- ), f"{node3_id} 应有3条边,实际有 {len(nodes_edges[node3_id])} 条"
-
- # 6.1 验证批量获取节点边的无向图特性
- print("== 验证批量获取节点边的无向图特性")
-
- # 检查节点1的边是否包含所有相关的边(无论方向)
- node1_outgoing_edges = [
- (src, tgt) for src, tgt in nodes_edges[node1_id] if src == node1_id
- ]
- node1_incoming_edges = [
- (src, tgt) for src, tgt in nodes_edges[node1_id] if tgt == node1_id
- ]
- print(f"节点 {node1_id} 的出边: {node1_outgoing_edges}")
- print(f"节点 {node1_id} 的入边: {node1_incoming_edges}")
-
- # 检查是否包含到机器学习、自然语言处理和计算机视觉的边
- has_edge_to_node2 = any(tgt == node2_id for _, tgt in node1_outgoing_edges)
- has_edge_to_node4 = any(tgt == node4_id for _, tgt in node1_outgoing_edges)
- has_edge_to_node5 = any(tgt == node5_id for _, tgt in node1_outgoing_edges)
-
- assert has_edge_to_node2, f"节点 {node1_id} 的边列表中应包含到 {node2_id} 的边"
- assert has_edge_to_node4, f"节点 {node1_id} 的边列表中应包含到 {node4_id} 的边"
- assert has_edge_to_node5, f"节点 {node1_id} 的边列表中应包含到 {node5_id} 的边"
-
- # 检查节点3的边是否包含所有相关的边(无论方向)
- node3_outgoing_edges = [
- (src, tgt) for src, tgt in nodes_edges[node3_id] if src == node3_id
- ]
- node3_incoming_edges = [
- (src, tgt) for src, tgt in nodes_edges[node3_id] if tgt == node3_id
- ]
- print(f"节点 {node3_id} 的出边: {node3_outgoing_edges}")
- print(f"节点 {node3_id} 的入边: {node3_incoming_edges}")
-
- # 检查是否包含与机器学习、自然语言处理和计算机视觉的连接(忽略方向)
- has_connection_with_node2 = any(
- (src == node2_id and tgt == node3_id)
- or (src == node3_id and tgt == node2_id)
- for src, tgt in nodes_edges[node3_id]
- )
- has_connection_with_node4 = any(
- (src == node3_id and tgt == node4_id)
- or (src == node4_id and tgt == node3_id)
- for src, tgt in nodes_edges[node3_id]
- )
- has_connection_with_node5 = any(
- (src == node3_id and tgt == node5_id)
- or (src == node5_id and tgt == node3_id)
- for src, tgt in nodes_edges[node3_id]
- )
-
- assert (
- has_connection_with_node2
- ), f"节点 {node3_id} 的边列表中应包含与 {node2_id} 的连接"
- assert (
- has_connection_with_node4
- ), f"节点 {node3_id} 的边列表中应包含与 {node4_id} 的连接"
- assert (
- has_connection_with_node5
- ), f"节点 {node3_id} 的边列表中应包含与 {node5_id} 的连接"
-
- print("无向图特性验证成功:批量获取的节点边包含所有相关的边(无论方向)")
-
- # 7. 测试 get_nodes_by_chunk_ids - 批量根据 chunk_ids 获取多个节点
- print("== 测试 get_nodes_by_chunk_ids")
-
- print("== 测试单个 chunk_id,匹配多个节点")
- nodes = await storage.get_nodes_by_chunk_ids([chunk2_id])
- assert len(nodes) == 2, f"{chunk1_id} 应有2个节点,实际有 {len(nodes)} 个"
-
- has_node1 = any(node["entity_id"] == node1_id for node in nodes)
- has_node2 = any(node["entity_id"] == node2_id for node in nodes)
-
- assert has_node1, f"节点 {node1_id} 应在返回结果中"
- assert has_node2, f"节点 {node2_id} 应在返回结果中"
-
- print("== 测试多个 chunk_id,部分匹配多个节点")
- nodes = await storage.get_nodes_by_chunk_ids([chunk2_id, chunk3_id])
- assert (
- len(nodes) == 3
- ), f"{chunk2_id}, {chunk3_id} 应有3个节点,实际有 {len(nodes)} 个"
-
- has_node1 = any(node["entity_id"] == node1_id for node in nodes)
- has_node2 = any(node["entity_id"] == node2_id for node in nodes)
- has_node3 = any(node["entity_id"] == node3_id for node in nodes)
-
- assert has_node1, f"节点 {node1_id} 应在返回结果中"
- assert has_node2, f"节点 {node2_id} 应在返回结果中"
- assert has_node3, f"节点 {node3_id} 应在返回结果中"
-
- # 8. 测试 get_edges_by_chunk_ids - 批量根据 chunk_ids 获取多条边
- print("== 测试 get_edges_by_chunk_ids")
-
- print("== 测试单个 chunk_id,匹配多条边")
- edges = await storage.get_edges_by_chunk_ids([chunk2_id])
- assert len(edges) == 2, f"{chunk2_id} 应有2条边,实际有 {len(edges)} 条"
-
- has_edge_node1_node2 = any(
- edge["source"] == node1_id and edge["target"] == node2_id for edge in edges
- )
- has_edge_node2_node3 = any(
- edge["source"] == node2_id and edge["target"] == node3_id for edge in edges
- )
-
- assert has_edge_node1_node2, f"{chunk2_id} 应包含 {node1_id} 到 {node2_id} 的边"
- assert has_edge_node2_node3, f"{chunk2_id} 应包含 {node2_id} 到 {node3_id} 的边"
-
- print("== 测试多个 chunk_id,部分匹配多条边")
- edges = await storage.get_edges_by_chunk_ids([chunk2_id, chunk3_id])
- assert (
- len(edges) == 3
- ), f"{chunk2_id}, {chunk3_id} 应有3条边,实际有 {len(edges)} 条"
-
- has_edge_node1_node2 = any(
- edge["source"] == node1_id and edge["target"] == node2_id for edge in edges
- )
- has_edge_node2_node3 = any(
- edge["source"] == node2_id and edge["target"] == node3_id for edge in edges
- )
- has_edge_node1_node4 = any(
- edge["source"] == node1_id and edge["target"] == node4_id for edge in edges
- )
-
- assert (
- has_edge_node1_node2
- ), f"{chunk2_id}, {chunk3_id} 应包含 {node1_id} 到 {node2_id} 的边"
- assert (
- has_edge_node2_node3
- ), f"{chunk2_id}, {chunk3_id} 应包含 {node2_id} 到 {node3_id} 的边"
- assert (
- has_edge_node1_node4
- ), f"{chunk2_id}, {chunk3_id} 应包含 {node1_id} 到 {node4_id} 的边"
-
- print("\n批量操作测试完成")
- return True
-
- except Exception as e:
- ASCIIColors.red(f"测试过程中发生错误: {str(e)}")
- return False
-
-
-async def test_graph_special_characters(storage):
- """
- 测试图数据库对特殊字符的处理:
- 1. 测试节点名称和描述中包含单引号、双引号和反斜杠
- 2. 测试边的描述中包含单引号、双引号和反斜杠
- 3. 验证特殊字符是否被正确保存和检索
- """
- try:
- # 1. 测试节点名称中的特殊字符
- node1_id = "包含'单引号'的节点"
- node1_data = {
- "entity_id": node1_id,
- "description": "这个描述包含'单引号'、\"双引号\"和\\反斜杠",
- "keywords": "特殊字符,引号,转义",
- "entity_type": "测试节点",
- }
- print(f"插入包含特殊字符的节点1: {node1_id}")
- await storage.upsert_node(node1_id, node1_data)
-
- # 2. 测试节点名称中的双引号
- node2_id = '包含"双引号"的节点'
- node2_data = {
- "entity_id": node2_id,
- "description": "这个描述同时包含'单引号'和\"双引号\"以及\\反斜杠\\路径",
- "keywords": "特殊字符,引号,JSON",
- "entity_type": "测试节点",
- }
- print(f"插入包含特殊字符的节点2: {node2_id}")
- await storage.upsert_node(node2_id, node2_data)
-
- # 3. 测试节点名称中的反斜杠
- node3_id = "包含\\反斜杠\\的节点"
- node3_data = {
- "entity_id": node3_id,
- "description": "这个描述包含Windows路径C:\\Program Files\\和转义字符\\n\\t",
- "keywords": "反斜杠,路径,转义",
- "entity_type": "测试节点",
- }
- print(f"插入包含特殊字符的节点3: {node3_id}")
- await storage.upsert_node(node3_id, node3_data)
-
- # 4. 测试边描述中的特殊字符
- edge1_data = {
- "relationship": "特殊'关系'",
- "weight": 1.0,
- "description": "这个边描述包含'单引号'、\"双引号\"和\\反斜杠",
- }
- print(f"插入包含特殊字符的边: {node1_id} -> {node2_id}")
- await storage.upsert_edge(node1_id, node2_id, edge1_data)
-
- # 5. 测试边描述中的更复杂特殊字符组合
- edge2_data = {
- "relationship": '复杂"关系"\\类型',
- "weight": 0.8,
- "description": "包含SQL注入尝试: SELECT * FROM users WHERE name='admin'--",
- }
- print(f"插入包含复杂特殊字符的边: {node2_id} -> {node3_id}")
- await storage.upsert_edge(node2_id, node3_id, edge2_data)
-
- # 6. 验证节点特殊字符是否正确保存
- print("\n== 验证节点特殊字符")
- for node_id, original_data in [
- (node1_id, node1_data),
- (node2_id, node2_data),
- (node3_id, node3_data),
- ]:
- node_props = await storage.get_node(node_id)
- if node_props:
- print(f"成功读取节点: {node_id}")
- print(f"节点描述: {node_props.get('description', '无描述')}")
-
- # 验证节点ID是否正确保存
- assert (
- node_props.get("entity_id") == node_id
- ), f"节点ID不匹配: 期望 {node_id}, 实际 {node_props.get('entity_id')}"
-
- # 验证描述是否正确保存
- assert (
- node_props.get("description") == original_data["description"]
- ), f"节点描述不匹配: 期望 {original_data['description']}, 实际 {node_props.get('description')}"
-
- print(f"节点 {node_id} 特殊字符验证成功")
- else:
- print(f"读取节点属性失败: {node_id}")
- assert False, f"未能读取节点属性: {node_id}"
-
- # 7. 验证边特殊字符是否正确保存
- print("\n== 验证边特殊字符")
- edge1_props = await storage.get_edge(node1_id, node2_id)
- if edge1_props:
- print(f"成功读取边: {node1_id} -> {node2_id}")
- print(f"边关系: {edge1_props.get('relationship', '无关系')}")
- print(f"边描述: {edge1_props.get('description', '无描述')}")
-
- # 验证边关系是否正确保存
- assert (
- edge1_props.get("relationship") == edge1_data["relationship"]
- ), f"边关系不匹配: 期望 {edge1_data['relationship']}, 实际 {edge1_props.get('relationship')}"
-
- # 验证边描述是否正确保存
- assert (
- edge1_props.get("description") == edge1_data["description"]
- ), f"边描述不匹配: 期望 {edge1_data['description']}, 实际 {edge1_props.get('description')}"
-
- print(f"边 {node1_id} -> {node2_id} 特殊字符验证成功")
- else:
- print(f"读取边属性失败: {node1_id} -> {node2_id}")
- assert False, f"未能读取边属性: {node1_id} -> {node2_id}"
-
- edge2_props = await storage.get_edge(node2_id, node3_id)
- if edge2_props:
- print(f"成功读取边: {node2_id} -> {node3_id}")
- print(f"边关系: {edge2_props.get('relationship', '无关系')}")
- print(f"边描述: {edge2_props.get('description', '无描述')}")
-
- # 验证边关系是否正确保存
- assert (
- edge2_props.get("relationship") == edge2_data["relationship"]
- ), f"边关系不匹配: 期望 {edge2_data['relationship']}, 实际 {edge2_props.get('relationship')}"
-
- # 验证边描述是否正确保存
- assert (
- edge2_props.get("description") == edge2_data["description"]
- ), f"边描述不匹配: 期望 {edge2_data['description']}, 实际 {edge2_props.get('description')}"
-
- print(f"边 {node2_id} -> {node3_id} 特殊字符验证成功")
- else:
- print(f"读取边属性失败: {node2_id} -> {node3_id}")
- assert False, f"未能读取边属性: {node2_id} -> {node3_id}"
-
- print("\n特殊字符测试完成,数据已保留在数据库中")
- return True
-
- except Exception as e:
- ASCIIColors.red(f"测试过程中发生错误: {str(e)}")
- return False
-
-
-async def test_graph_undirected_property(storage):
- """
- 专门测试图存储的无向图特性:
- 1. 验证插入一个方向的边后,反向查询是否能获得相同的结果
- 2. 验证边的属性在正向和反向查询中是否一致
- 3. 验证删除一个方向的边后,另一个方向的边是否也被删除
- 4. 验证批量操作中的无向图特性
- """
- try:
- # 1. 插入测试数据
- # 插入节点1: 计算机科学
- node1_id = "计算机科学"
- node1_data = {
- "entity_id": node1_id,
- "description": "计算机科学是研究计算机及其应用的科学。",
- "keywords": "计算机,科学,技术",
- "entity_type": "学科",
- }
- print(f"插入节点1: {node1_id}")
- await storage.upsert_node(node1_id, node1_data)
-
- # 插入节点2: 数据结构
- node2_id = "数据结构"
- node2_data = {
- "entity_id": node2_id,
- "description": "数据结构是计算机科学中的一个基础概念,用于组织和存储数据。",
- "keywords": "数据,结构,组织",
- "entity_type": "概念",
- }
- print(f"插入节点2: {node2_id}")
- await storage.upsert_node(node2_id, node2_data)
-
- # 插入节点3: 算法
- node3_id = "算法"
- node3_data = {
- "entity_id": node3_id,
- "description": "算法是解决问题的步骤和方法。",
- "keywords": "算法,步骤,方法",
- "entity_type": "概念",
- }
- print(f"插入节点3: {node3_id}")
- await storage.upsert_node(node3_id, node3_data)
-
- # 2. 测试插入边后的无向图特性
- print("\n== 测试插入边后的无向图特性")
-
- # 插入边1: 计算机科学 -> 数据结构
- edge1_data = {
- "relationship": "包含",
- "weight": 1.0,
- "description": "计算机科学包含数据结构这个概念",
- }
- print(f"插入边1: {node1_id} -> {node2_id}")
- await storage.upsert_edge(node1_id, node2_id, edge1_data)
-
- # 验证正向查询
- forward_edge = await storage.get_edge(node1_id, node2_id)
- print(f"正向边属性: {forward_edge}")
- assert forward_edge is not None, f"未能读取正向边属性: {node1_id} -> {node2_id}"
-
- # 验证反向查询
- reverse_edge = await storage.get_edge(node2_id, node1_id)
- print(f"反向边属性: {reverse_edge}")
- assert reverse_edge is not None, f"未能读取反向边属性: {node2_id} -> {node1_id}"
-
- # 验证正向和反向边属性是否一致
- assert (
- forward_edge == reverse_edge
- ), "正向和反向边属性不一致,无向图特性验证失败"
- print("无向图特性验证成功:正向和反向边属性一致")
-
- # 3. 测试边的度数的无向图特性
- print("\n== 测试边的度数的无向图特性")
-
- # 插入边2: 计算机科学 -> 算法
- edge2_data = {
- "relationship": "包含",
- "weight": 1.0,
- "description": "计算机科学包含算法这个概念",
- }
- print(f"插入边2: {node1_id} -> {node3_id}")
- await storage.upsert_edge(node1_id, node3_id, edge2_data)
-
- # 验证正向和反向边的度数
- forward_degree = await storage.edge_degree(node1_id, node2_id)
- reverse_degree = await storage.edge_degree(node2_id, node1_id)
- print(f"正向边 {node1_id} -> {node2_id} 的度数: {forward_degree}")
- print(f"反向边 {node2_id} -> {node1_id} 的度数: {reverse_degree}")
- assert (
- forward_degree == reverse_degree
- ), "正向和反向边的度数不一致,无向图特性验证失败"
- print("无向图特性验证成功:正向和反向边的度数一致")
-
- # 4. 测试删除边的无向图特性
- print("\n== 测试删除边的无向图特性")
-
- # 删除正向边
- print(f"删除边: {node1_id} -> {node2_id}")
- await storage.remove_edges([(node1_id, node2_id)])
-
- # 验证正向边是否被删除
- forward_edge = await storage.get_edge(node1_id, node2_id)
- print(f"删除后查询正向边属性 {node1_id} -> {node2_id}: {forward_edge}")
- assert forward_edge is None, f"边 {node1_id} -> {node2_id} 应已被删除"
-
- # 验证反向边是否也被删除
- reverse_edge = await storage.get_edge(node2_id, node1_id)
- print(f"删除后查询反向边属性 {node2_id} -> {node1_id}: {reverse_edge}")
- assert (
- reverse_edge is None
- ), f"反向边 {node2_id} -> {node1_id} 也应被删除,无向图特性验证失败"
- print("无向图特性验证成功:删除一个方向的边后,反向边也被删除")
-
- # 5. 测试批量操作中的无向图特性
- print("\n== 测试批量操作中的无向图特性")
-
- # 重新插入边
- await storage.upsert_edge(node1_id, node2_id, edge1_data)
-
- # 批量获取边属性
- edge_dicts = [
- {"src": node1_id, "tgt": node2_id},
- {"src": node1_id, "tgt": node3_id},
- ]
- reverse_edge_dicts = [
- {"src": node2_id, "tgt": node1_id},
- {"src": node3_id, "tgt": node1_id},
- ]
-
- edges_dict = await storage.get_edges_batch(edge_dicts)
- reverse_edges_dict = await storage.get_edges_batch(reverse_edge_dicts)
-
- print(f"批量获取正向边属性结果: {edges_dict.keys()}")
- print(f"批量获取反向边属性结果: {reverse_edges_dict.keys()}")
-
- # 验证正向和反向边的属性是否一致
- for (src, tgt), props in edges_dict.items():
- assert (
- tgt,
- src,
- ) in reverse_edges_dict, f"反向边 {tgt} -> {src} 应在返回结果中"
- assert (
- props == reverse_edges_dict[(tgt, src)]
- ), f"边 {src} -> {tgt} 和反向边 {tgt} -> {src} 的属性不一致"
-
- print("无向图特性验证成功:批量获取的正向和反向边属性一致")
-
- # 6. 测试批量获取节点边的无向图特性
- print("\n== 测试批量获取节点边的无向图特性")
-
- nodes_edges = await storage.get_nodes_edges_batch([node1_id, node2_id])
- print(f"批量获取节点边结果: {nodes_edges.keys()}")
-
- # 检查节点1的边是否包含所有相关的边(无论方向)
- node1_edges = nodes_edges[node1_id]
- node2_edges = nodes_edges[node2_id]
-
- # 检查节点1是否有到节点2和节点3的边
- has_edge_to_node2 = any(
- (src == node1_id and tgt == node2_id) for src, tgt in node1_edges
- )
- has_edge_to_node3 = any(
- (src == node1_id and tgt == node3_id) for src, tgt in node1_edges
- )
-
- assert has_edge_to_node2, f"节点 {node1_id} 的边列表中应包含到 {node2_id} 的边"
- assert has_edge_to_node3, f"节点 {node1_id} 的边列表中应包含到 {node3_id} 的边"
-
- # 检查节点2是否有到节点1的边
- has_edge_to_node1 = any(
- (src == node2_id and tgt == node1_id)
- or (src == node1_id and tgt == node2_id)
- for src, tgt in node2_edges
- )
- assert (
- has_edge_to_node1
- ), f"节点 {node2_id} 的边列表中应包含与 {node1_id} 的连接"
-
- print("无向图特性验证成功:批量获取的节点边包含所有相关的边(无论方向)")
-
- print("\n无向图特性测试完成")
- return True
-
- except Exception as e:
- ASCIIColors.red(f"测试过程中发生错误: {str(e)}")
- return False
-
-
-async def main():
- """主函数"""
- # 显示程序标题
- ASCIIColors.cyan("""
- ╔══════════════════════════════════════════════════════════════╗
- ║ 通用图存储测试程序 ║
- ╚══════════════════════════════════════════════════════════════╝
- """)
-
- # 检查.env文件
- if not check_env_file():
- return
-
- # 加载环境变量
- load_dotenv(dotenv_path=".env", override=False)
-
- # 获取图存储类型
- graph_storage_type = os.getenv("LIGHTRAG_GRAPH_STORAGE", "NetworkXStorage")
- ASCIIColors.magenta(f"\n当前配置的图存储类型: {graph_storage_type}")
- ASCIIColors.white(
- f"支持的图存储类型: {', '.join(STORAGE_IMPLEMENTATIONS['GRAPH_STORAGE']['implementations'])}"
- )
-
- # 初始化存储实例
- storage = await initialize_graph_storage()
- if not storage:
- ASCIIColors.red("初始化存储实例失败,测试程序退出")
- return
-
- try:
- # 显示测试选项
- ASCIIColors.yellow("\n请选择测试类型:")
- ASCIIColors.white("1. 基本测试 (节点和边的插入、读取)")
- ASCIIColors.white("2. 高级测试 (度数、标签、知识图谱、删除操作等)")
- ASCIIColors.white("3. 批量操作测试 (批量获取节点、边属性和度数等)")
- ASCIIColors.white("4. 无向图特性测试 (验证存储的无向图特性)")
- ASCIIColors.white("5. 特殊字符测试 (验证单引号、双引号和反斜杠等特殊字符)")
- ASCIIColors.white("6. 全部测试")
-
- choice = input("\n请输入选项 (1/2/3/4/5/6): ")
-
- # 在执行测试前清理数据
- if choice in ["1", "2", "3", "4", "5", "6"]:
- ASCIIColors.yellow("\n执行测试前清理数据...")
- await storage.drop()
- ASCIIColors.green("数据清理完成\n")
-
- if choice == "1":
- await test_graph_basic(storage)
- elif choice == "2":
- await test_graph_advanced(storage)
- elif choice == "3":
- await test_graph_batch_operations(storage)
- elif choice == "4":
- await test_graph_undirected_property(storage)
- elif choice == "5":
- await test_graph_special_characters(storage)
- elif choice == "6":
- ASCIIColors.cyan("\n=== 开始基本测试 ===")
- basic_result = await test_graph_basic(storage)
-
- if basic_result:
- ASCIIColors.cyan("\n=== 开始高级测试 ===")
- advanced_result = await test_graph_advanced(storage)
-
- if advanced_result:
- ASCIIColors.cyan("\n=== 开始批量操作测试 ===")
- batch_result = await test_graph_batch_operations(storage)
-
- if batch_result:
- ASCIIColors.cyan("\n=== 开始无向图特性测试 ===")
- undirected_result = await test_graph_undirected_property(
- storage
- )
-
- if undirected_result:
- ASCIIColors.cyan("\n=== 开始特殊字符测试 ===")
- await test_graph_special_characters(storage)
- else:
- ASCIIColors.red("无效的选项")
-
- finally:
- # 关闭连接
- if storage:
- await storage.finalize()
- ASCIIColors.green("\n存储连接已关闭")
-
-
-if __name__ == "__main__":
- asyncio.run(main())
+from tests.graph.tests.basic import test_graph_basic
+from tests.graph.tests.advanced import test_graph_advanced
+from tests.graph.tests.batch import test_graph_batch_operations
+from tests.graph.tests.special_chars import test_graph_special_characters
+from tests.graph.tests.undirected import test_graph_undirected_property
+
+
+@pytest.fixture
+async def storage():
+ """Fixture to provide a storage instance for tests"""
+ storage_instance = await initialize_graph_test_storage()
+ if storage_instance is None:
+ pytest.skip("Failed to initialize storage")
+
+ yield storage_instance
+
+ # Cleanup
+ if storage_instance and hasattr(storage_instance, "close"):
+ await storage_instance.close()
+ if storage_instance and hasattr(storage_instance, "_temp_dir"):
+ cleanup_kuzu_test_environment(storage_instance._temp_dir)
+
+
+@pytest.mark.asyncio
+async def test_basic_graph_operations(storage):
+ """Test basic graph operations"""
+ os.environ["TEST_LANGUAGE"] = "english"
+ result = await test_graph_basic(storage)
+ assert result is True, "Basic graph operations test failed"
+
+
+@pytest.mark.asyncio
+async def test_advanced_graph_operations(storage):
+ """Test advanced graph operations"""
+ os.environ["TEST_LANGUAGE"] = "english"
+ result = await test_graph_advanced(storage)
+ assert result is True, "Advanced graph operations test failed"
+
+
+@pytest.mark.asyncio
+async def test_batch_graph_operations(storage):
+ """Test batch graph operations"""
+ os.environ["TEST_LANGUAGE"] = "english"
+ result = await test_graph_batch_operations(storage)
+ assert result is True, "Batch graph operations test failed"
+
+
+@pytest.mark.asyncio
+async def test_special_characters_handling(storage):
+ """Test special characters handling"""
+ os.environ["TEST_LANGUAGE"] = "english"
+ result = await test_graph_special_characters(storage)
+ assert result is True, "Special characters handling test failed"
+
+
+@pytest.mark.asyncio
+async def test_undirected_graph_properties(storage):
+ """Test undirected graph properties"""
+ os.environ["TEST_LANGUAGE"] = "english"
+ result = await test_graph_undirected_property(storage)
+ assert result is True, "Undirected graph properties test failed"
+
+
+# Chinese language variants
+@pytest.mark.asyncio
+async def test_basic_graph_operations_chinese(storage):
+ """Test basic graph operations with Chinese translations"""
+ os.environ["TEST_LANGUAGE"] = "chinese"
+ result = await test_graph_basic(storage)
+ assert result is True, "Basic graph operations test (Chinese) failed"
+
+
+@pytest.mark.asyncio
+async def test_special_characters_handling_chinese(storage):
+ """Test special characters handling with Chinese translations"""
+ os.environ["TEST_LANGUAGE"] = "chinese"
+ result = await test_graph_special_characters(storage)
+ assert result is True, "Special characters handling test (Chinese) failed"
diff --git a/tests/test_kuzu_impl.py b/tests/test_kuzu_impl.py
new file mode 100644
index 0000000000..942b1a1885
--- /dev/null
+++ b/tests/test_kuzu_impl.py
@@ -0,0 +1,1215 @@
+import asyncio
+import tempfile
+import os
+import pytest
+from unittest.mock import MagicMock
+from dotenv import load_dotenv
+from lightrag.kg.kuzu_impl import KuzuDBStorage
+from lightrag.types import KnowledgeGraph
+
+load_dotenv(dotenv_path=".env", override=False)
+
+
+class TestKuzuDBStorage:
+ """Test suite for KuzuDBStorage implementation"""
+
+ @pytest.fixture
+ async def storage(self):
+ """Create a test storage instance with temporary database"""
+ with tempfile.TemporaryDirectory() as temp_dir:
+ os.environ["KUZU_DB_PATH"] = os.path.join(temp_dir, "test.db")
+
+ # Mock embedding function
+ embedding_func = MagicMock(return_value=[0.1, 0.2, 0.3])
+
+ storage = KuzuDBStorage(
+ namespace="test",
+ global_config={"max_graph_nodes": 1000},
+ embedding_func=embedding_func,
+ workspace=os.environ.get("KUZU_WORKSPACE", "kuzudb"),
+ )
+
+ await storage.initialize()
+ yield storage
+ await storage.finalize()
+
+ async def test_initialization(self, storage):
+ """Test database initialization"""
+ assert storage._db is not None
+ assert storage._conn is not None
+ # Get the actual workspace value rather than assuming from environment
+ expected_workspace = storage.workspace or "base"
+ assert storage._get_label() == expected_workspace
+
+ async def test_node_operations(self, storage):
+ """Test node creation, retrieval, and existence checking"""
+ # Test node doesn't exist initially
+ # assert await storage.has_node("test_node_1") == False
+ if not await storage.has_node("test_node_1"):
+ assert True
+ else:
+ assert False
+
+ # Create a node
+ node_data = {
+ "entity_id": "test_node_1",
+ "entity_type": "Person",
+ "description": "A test person",
+ "source_id": "chunk_1",
+ }
+ await storage.upsert_node("test_node_1", node_data)
+
+ # Test node exists now
+ # assert await storage.has_node("test_node_1") == True
+ if await storage.has_node("test_node_1"):
+ assert True
+ else:
+ assert False
+
+ # Retrieve the node
+ retrieved_node = await storage.get_node("test_node_1")
+ assert retrieved_node is not None
+ assert retrieved_node["entity_id"] == "test_node_1"
+ assert retrieved_node["entity_type"] == "Person"
+ assert retrieved_node["description"] == "A test person"
+
+ async def test_edge_operations(self, storage):
+ """Test edge creation, retrieval, and existence checking"""
+ # Create two nodes first
+ node1_data = {
+ "entity_id": "node_1",
+ "entity_type": "Person",
+ "description": "First person",
+ "source_id": "chunk_1",
+ }
+ node2_data = {
+ "entity_id": "node_2",
+ "entity_type": "Person",
+ "description": "Second person",
+ "source_id": "chunk_1",
+ }
+
+ await storage.upsert_node("node_1", node1_data)
+ await storage.upsert_node("node_2", node2_data)
+
+ # Test edge doesn't exist initially
+ # assert await storage.has_edge("node_1", "node_2") == False
+ if not await storage.has_edge("node_1", "node_2"):
+ assert True
+ else:
+ assert False
+
+ # Create an edge
+ edge_data = {
+ "weight": 0.8,
+ "description": "knows",
+ "keywords": "relationship",
+ "source_id": "chunk_1",
+ }
+ await storage.upsert_edge("node_1", "node_2", edge_data)
+
+ # Test edge exists now
+ # assert await storage.has_edge("node_1", "node_2") == True
+ if await storage.has_edge("node_1", "node_2"):
+ assert True
+ else:
+ assert False
+
+ # Retrieve the edge
+ retrieved_edge = await storage.get_edge("node_1", "node_2")
+ assert retrieved_edge is not None
+ assert retrieved_edge["weight"] == 0.8
+ assert retrieved_edge["description"] == "knows"
+
+ async def test_node_degree(self, storage):
+ """Test node degree calculation"""
+ # Create nodes
+ await storage.upsert_node(
+ "center_node",
+ {
+ "entity_id": "center_node",
+ "entity_type": "Person",
+ "description": "Center node",
+ "source_id": "chunk_1",
+ },
+ )
+
+ await storage.upsert_node(
+ "connected_node_1",
+ {
+ "entity_id": "connected_node_1",
+ "entity_type": "Person",
+ "description": "Connected node 1",
+ "source_id": "chunk_1",
+ },
+ )
+
+ await storage.upsert_node(
+ "connected_node_2",
+ {
+ "entity_id": "connected_node_2",
+ "entity_type": "Person",
+ "description": "Connected node 2",
+ "source_id": "chunk_1",
+ },
+ )
+
+ # Initially degree should be 0
+ assert await storage.node_degree("center_node") == 0
+
+ # Add edges
+ await storage.upsert_edge(
+ "center_node",
+ "connected_node_1",
+ {"weight": 0.5, "description": "edge1", "source_id": "chunk_1"},
+ )
+
+ await storage.upsert_edge(
+ "center_node",
+ "connected_node_2",
+ {"weight": 0.7, "description": "edge2", "source_id": "chunk_1"},
+ )
+
+ # Now degree should be 2
+ degree = await storage.node_degree("center_node")
+ assert degree == 2
+
+ async def test_get_node_edges(self, storage):
+ """Test retrieving edges for a node"""
+ # Create test nodes
+ node_data = {
+ "entity_id": "main_node",
+ "entity_type": "Person",
+ "description": "Main node",
+ "source_id": "chunk_1",
+ }
+ await storage.upsert_node("main_node", node_data)
+
+ # Create connected nodes
+ for i in range(3):
+ await storage.upsert_node(
+ f"connected_{i}",
+ {
+ "entity_id": f"connected_{i}",
+ "entity_type": "Person",
+ "description": f"Connected node {i}",
+ "source_id": "chunk_1",
+ },
+ )
+
+ # Create edge
+ await storage.upsert_edge(
+ "main_node",
+ f"connected_{i}",
+ {
+ "weight": 0.5 + i * 0.1,
+ "description": f"edge to {i}",
+ "source_id": "chunk_1",
+ },
+ )
+
+ # Get edges for main node
+ edges = await storage.get_node_edges("main_node")
+ assert edges is not None
+ assert len(edges) == 3
+
+ # Check that all expected connections are present
+ edge_targets = [edge[1] for edge in edges if edge[0] == "main_node"]
+ edge_sources = [edge[0] for edge in edges if edge[1] == "main_node"]
+
+ connected_nodes = set(edge_targets + edge_sources)
+ expected_nodes = {"connected_0", "connected_1", "connected_2", "main_node"}
+ assert connected_nodes.issubset(expected_nodes)
+
+ async def test_batch_operations(self, storage):
+ """Test batch retrieval operations"""
+ # Create multiple nodes
+ node_ids = ["batch_node_1", "batch_node_2", "batch_node_3"]
+ for node_id in node_ids:
+ await storage.upsert_node(
+ node_id,
+ {
+ "entity_id": node_id,
+ "entity_type": "Person",
+ "description": f"Batch node {node_id}",
+ "source_id": "chunk_1",
+ },
+ )
+
+ # Test batch node retrieval
+ nodes = await storage.get_nodes_batch(node_ids)
+ assert len(nodes) == 3
+ for node_id in node_ids:
+ assert node_id in nodes
+ assert nodes[node_id]["entity_id"] == node_id
+
+ # Test batch degree retrieval
+ degrees = await storage.node_degrees_batch(node_ids)
+ assert len(degrees) == 3
+ for node_id in node_ids:
+ assert node_id in degrees
+ assert degrees[node_id] == 0 # No edges created yet
+
+ async def test_chunk_id_queries(self, storage):
+ """Test querying by chunk IDs"""
+ # Create nodes with specific chunk IDs
+ chunk_id = "test_chunk_123"
+
+ await storage.upsert_node(
+ "chunk_node_1",
+ {
+ "entity_id": "chunk_node_1",
+ "entity_type": "Person",
+ "description": "Node from chunk",
+ "source_id": chunk_id,
+ },
+ )
+
+ await storage.upsert_node(
+ "chunk_node_2",
+ {
+ "entity_id": "chunk_node_2",
+ "entity_type": "Person",
+ "description": "Another node from chunk",
+ "source_id": chunk_id,
+ },
+ )
+
+ # Create edge with same chunk ID
+ await storage.upsert_edge(
+ "chunk_node_1",
+ "chunk_node_2",
+ {"weight": 0.9, "description": "chunk edge", "source_id": chunk_id},
+ )
+
+ # Query nodes by chunk ID
+ nodes = await storage.get_nodes_by_chunk_ids([chunk_id])
+ assert len(nodes) >= 2
+
+ # Check that nodes have the correct chunk ID
+ for node in nodes:
+ assert chunk_id in node["source_id"]
+
+ # Query edges by chunk ID
+ edges = await storage.get_edges_by_chunk_ids([chunk_id])
+ assert len(edges) >= 1
+
+ # Check that edges have the correct chunk ID
+ for edge in edges:
+ assert chunk_id in edge["source_id"]
+
+ async def test_get_all_labels(self, storage):
+ """Test retrieving all node labels"""
+ # Create some test nodes
+ test_nodes = ["label_test_1", "label_test_2", "label_test_3"]
+ for node_id in test_nodes:
+ await storage.upsert_node(
+ node_id,
+ {
+ "entity_id": node_id,
+ "entity_type": "TestType",
+ "description": f"Test node {node_id}",
+ "source_id": "chunk_1",
+ },
+ )
+
+ # Get all labels
+ labels = await storage.get_all_labels()
+
+ # Check that our test nodes are in the labels
+ for node_id in test_nodes:
+ assert node_id in labels
+
+ async def test_get_all_nodes(self, storage):
+ """Test retrieving all nodes from the database"""
+ # Clear any existing data first
+ await storage.drop()
+
+ # Test empty database
+ all_nodes = await storage.get_all_nodes()
+ assert isinstance(all_nodes, list)
+ assert len(all_nodes) == 0
+
+ # Create test nodes with various properties
+ test_nodes = [
+ {
+ "entity_id": "node_all_1",
+ "entity_type": "Person",
+ "description": "First test person",
+ "keywords": "test,person,first",
+ "source_id": "chunk_1",
+ },
+ {
+ "entity_id": "node_all_2",
+ "entity_type": "Organization",
+ "description": "Test organization",
+ "keywords": "test,org,company",
+ "source_id": "chunk_2",
+ },
+ {
+ "entity_id": "node_all_3",
+ "entity_type": "Location",
+ "description": "Test location",
+ "keywords": "test,place,location",
+ "source_id": "chunk_3",
+ },
+ ]
+
+ # Insert test nodes
+ for node_data in test_nodes:
+ await storage.upsert_node(node_data["entity_id"], node_data)
+
+ # Get all nodes
+ all_nodes = await storage.get_all_nodes()
+
+ # Verify correct number of nodes returned
+ assert len(all_nodes) == 3
+
+ # Verify all nodes are returned with correct properties
+ returned_node_ids = {node["entity_id"] for node in all_nodes}
+ expected_node_ids = {node["entity_id"] for node in test_nodes}
+ assert returned_node_ids == expected_node_ids
+
+ # Verify node properties are correctly preserved
+ for returned_node in all_nodes:
+ original_node = next(
+ node
+ for node in test_nodes
+ if node["entity_id"] == returned_node["entity_id"]
+ )
+ assert returned_node["entity_type"] == original_node["entity_type"]
+ assert returned_node["description"] == original_node["description"]
+ assert returned_node["keywords"] == original_node["keywords"]
+ assert returned_node["source_id"] == original_node["source_id"]
+
+ print("✓ get_all_nodes test passed")
+
+ async def test_get_all_edges(self, storage):
+ """Test retrieving all edges from the database"""
+ # Clear any existing data first
+ await storage.drop()
+
+ # Test empty database
+ all_edges = await storage.get_all_edges()
+ assert isinstance(all_edges, list)
+ assert len(all_edges) == 0
+
+ # Create test nodes
+ nodes = [
+ {
+ "entity_id": "edge_node_1",
+ "entity_type": "Person",
+ "description": "First person",
+ "source_id": "chunk_1",
+ },
+ {
+ "entity_id": "edge_node_2",
+ "entity_type": "Person",
+ "description": "Second person",
+ "source_id": "chunk_1",
+ },
+ {
+ "entity_id": "edge_node_3",
+ "entity_type": "Organization",
+ "description": "Test org",
+ "source_id": "chunk_2",
+ },
+ ]
+
+ for node in nodes:
+ await storage.upsert_node(node["entity_id"], node)
+
+ # Create test edges with various properties
+ test_edges = [
+ {
+ "source": "edge_node_1",
+ "target": "edge_node_2",
+ "weight": 0.9,
+ "description": "knows personally",
+ "keywords": "personal,relationship",
+ "source_id": "chunk_1",
+ },
+ {
+ "source": "edge_node_1",
+ "target": "edge_node_3",
+ "weight": 0.7,
+ "description": "works for",
+ "keywords": "professional,employment",
+ "source_id": "chunk_2",
+ },
+ {
+ "source": "edge_node_2",
+ "target": "edge_node_3",
+ "weight": 0.6,
+ "description": "collaborates with",
+ "keywords": "professional,collaboration",
+ "source_id": "chunk_3",
+ },
+ ]
+
+ # Insert test edges
+ for edge_data in test_edges:
+ source = edge_data.pop("source")
+ target = edge_data.pop("target")
+ await storage.upsert_edge(source, target, edge_data)
+
+ # Get all edges
+ all_edges = await storage.get_all_edges()
+
+ # Verify correct number of edges (should handle bidirectional properly)
+ assert len(all_edges) == 3
+
+ # Verify edge properties and normalize bidirectional edges
+ expected_pairs = {
+ ("edge_node_1", "edge_node_2"),
+ ("edge_node_1", "edge_node_3"),
+ ("edge_node_2", "edge_node_3"),
+ }
+ returned_pairs = set()
+
+ for edge in all_edges:
+ # Normalize edge pair (since edges are bidirectional)
+ edge_pair = tuple(sorted([edge["source"], edge["target"]]))
+ returned_pairs.add(edge_pair)
+
+ # Verify edge has required properties
+ assert "weight" in edge
+ assert "description" in edge
+ assert "keywords" in edge
+ assert "source_id" in edge
+ assert isinstance(edge["weight"], (int, float))
+
+ # Verify all expected edge pairs are present
+ assert returned_pairs == expected_pairs
+
+ # Verify specific edge properties
+ for edge in all_edges:
+ source, target = edge["source"], edge["target"]
+ edge_pair = tuple(sorted([source, target]))
+
+ if edge_pair == ("edge_node_1", "edge_node_2"):
+ assert edge["weight"] == 0.9
+ assert edge["description"] == "knows personally"
+ assert edge["keywords"] == "personal,relationship"
+ elif edge_pair == ("edge_node_1", "edge_node_3"):
+ assert edge["weight"] == 0.7
+ assert edge["description"] == "works for"
+ assert edge["keywords"] == "professional,employment"
+ elif edge_pair == ("edge_node_2", "edge_node_3"):
+ assert edge["weight"] == 0.6
+ assert edge["description"] == "collaborates with"
+ assert edge["keywords"] == "professional,collaboration"
+
+ print("✓ get_all_edges test passed")
+
+ async def test_get_popular_labels(self, storage):
+ """Test retrieving popular node labels by degree"""
+ # Clear any existing data first
+ await storage.drop()
+
+ # Test empty database
+ popular_labels = await storage.get_popular_labels(top_k=5)
+ assert isinstance(popular_labels, list)
+ assert len(popular_labels) == 0
+
+ # Create nodes with different degrees
+ # Hub node - will have highest degree (connected to 4 others)
+ await storage.upsert_node(
+ "hub_node",
+ {
+ "entity_id": "hub_node",
+ "entity_type": "Hub",
+ "description": "Central hub",
+ "source_id": "chunk_hub",
+ },
+ )
+
+ # Popular node - connected to 2 others
+ await storage.upsert_node(
+ "popular_node",
+ {
+ "entity_id": "popular_node",
+ "entity_type": "Popular",
+ "description": "Somewhat popular",
+ "source_id": "chunk_pop",
+ },
+ )
+
+ # Regular nodes - connected to 1 other each
+ regular_nodes = ["regular_1", "regular_2", "regular_3"]
+ for node_id in regular_nodes:
+ await storage.upsert_node(
+ node_id,
+ {
+ "entity_id": node_id,
+ "entity_type": "Regular",
+ "description": f"Regular node {node_id}",
+ "source_id": "chunk_reg",
+ },
+ )
+
+ # Isolated node - no connections (degree 0)
+ await storage.upsert_node(
+ "isolated_node",
+ {
+ "entity_id": "isolated_node",
+ "entity_type": "Isolated",
+ "description": "Isolated node",
+ "source_id": "chunk_iso",
+ },
+ )
+
+ # Create edges to establish different degrees
+ # Hub node connections (degree 4)
+ hub_connections = ["popular_node", "regular_1", "regular_2", "regular_3"]
+ for target in hub_connections:
+ await storage.upsert_edge(
+ "hub_node",
+ target,
+ {
+ "weight": 0.8,
+ "description": f"connects to {target}",
+ "source_id": "chunk_hub",
+ },
+ )
+
+ # Popular node additional connection (degree 2 total)
+ await storage.upsert_edge(
+ "popular_node",
+ "regular_1",
+ {
+ "weight": 0.6,
+ "description": "additional connection",
+ "source_id": "chunk_pop",
+ },
+ )
+
+ # Test default top_k (10)
+ popular_labels = await storage.get_popular_labels()
+ assert len(popular_labels) <= 10
+
+ # Verify ordering: hub_node should be first (highest degree)
+ assert popular_labels[0] == "hub_node"
+
+ # Popular_node should be second (second highest degree)
+ assert popular_labels[1] == "popular_node"
+
+ # Regular nodes should follow (degree 1 each), ordered by entity_id
+ regular_positions = [popular_labels.index(node) for node in regular_nodes]
+ assert all(pos > 1 for pos in regular_positions) # All after popular_node
+
+ # Isolated node should be last (degree 0)
+ assert popular_labels[-1] == "isolated_node"
+
+ # Test specific top_k limit
+ top_3 = await storage.get_popular_labels(top_k=3)
+ assert len(top_3) == 3
+ assert top_3[0] == "hub_node"
+ assert top_3[1] == "popular_node"
+ assert top_3[2] in regular_nodes # One of the regular nodes
+
+ # Test top_k larger than available nodes
+ all_labels = await storage.get_popular_labels(top_k=20)
+ assert len(all_labels) == 6 # Total nodes we created
+
+ # Test top_k = 1
+ top_1 = await storage.get_popular_labels(top_k=1)
+ assert len(top_1) == 1
+ assert top_1[0] == "hub_node"
+
+ print("✓ get_popular_labels test passed")
+
+ async def test_search_labels(self, storage):
+ """Test searching for node labels with various queries"""
+ # Clear any existing data first
+ await storage.drop()
+
+ # Test empty database
+ search_results = await storage.search_labels("test")
+ assert isinstance(search_results, list)
+ assert len(search_results) == 0
+
+ # Create diverse test nodes with searchable content
+ test_nodes = [
+ {
+ "entity_id": "john_doe",
+ "entity_type": "Person",
+ "description": "Software engineer at tech company",
+ "keywords": "python,programming,software",
+ "source_id": "chunk_1",
+ },
+ {
+ "entity_id": "jane_smith",
+ "entity_type": "Person",
+ "description": "Data scientist specializing in machine learning",
+ "keywords": "data,science,ml,python",
+ "source_id": "chunk_2",
+ },
+ {
+ "entity_id": "acme_corp",
+ "entity_type": "Organization",
+ "description": "Technology company developing software solutions",
+ "keywords": "tech,software,company,business",
+ "source_id": "chunk_3",
+ },
+ {
+ "entity_id": "silicon_valley",
+ "entity_type": "Location",
+ "description": "Technology hub in California",
+ "keywords": "tech,california,innovation,startups",
+ "source_id": "chunk_4",
+ },
+ {
+ "entity_id": "machine_learning_project",
+ "entity_type": "Project",
+ "description": "Research project on deep learning algorithms",
+ "keywords": "ml,ai,research,algorithms,deep_learning",
+ "source_id": "chunk_5",
+ },
+ ]
+
+ # Insert test nodes
+ for node in test_nodes:
+ await storage.upsert_node(node["entity_id"], node)
+
+ # Test 1: Search by entity_id substring
+ results = await storage.search_labels("john")
+ assert "john_doe" in results
+ assert len([r for r in results if "john" in r.lower()]) >= 1
+
+ # Test 2: Search by description content
+ results = await storage.search_labels("software")
+ expected_matches = {"john_doe", "acme_corp"} # Both mention software
+ actual_matches = set(results)
+ assert expected_matches.issubset(actual_matches)
+
+ # Test 3: Search by keywords
+ results = await storage.search_labels("python")
+ expected_matches = {"john_doe", "jane_smith"} # Both have python keyword
+ actual_matches = set(results)
+ assert expected_matches.issubset(actual_matches)
+
+ # Test 4: Search for technology-related terms
+ results = await storage.search_labels("tech")
+ expected_matches = {"acme_corp", "silicon_valley"} # Both have tech keyword
+ actual_matches = set(results)
+ assert expected_matches.issubset(actual_matches)
+
+ # Test 5: Search with no matches
+ results = await storage.search_labels("nonexistent_term_xyz")
+ assert len(results) == 0
+
+ # Test 6: Case sensitive search (KuzuDB CONTAINS is case sensitive)
+ results_tech_desc = await storage.search_labels(
+ "Technology"
+ ) # Capital T, matches description
+ results_tech_keyword = await storage.search_labels(
+ "tech"
+ ) # Lowercase, matches keywords
+ # Both should return results but may be different
+ assert (
+ len(results_tech_desc) > 0
+ ) # Should find acme_corp and silicon_valley descriptions
+ assert len(results_tech_keyword) > 0 # Should find tech keywords
+
+ # Test 7: Search with limit parameter
+ results_limited = await storage.search_labels("tech", limit=2)
+ assert len(results_limited) <= 2
+ assert len(results_limited) > 0 # Should find at least some matches
+
+ # Test 8: Search for machine learning related content
+ results = await storage.search_labels("machine learning")
+ expected_matches = {
+ "jane_smith"
+ } # Only jane_smith has "machine learning" in description
+ actual_matches = set(results)
+ assert expected_matches.issubset(actual_matches)
+
+ # Test 8b: Search for deep learning related content
+ results = await storage.search_labels("deep learning")
+ expected_matches = {
+ "machine_learning_project"
+ } # Only machine_learning_project has "deep learning"
+ actual_matches = set(results)
+ assert expected_matches.issubset(actual_matches)
+
+ # Test 9: Search for partial entity_id match
+ results = await storage.search_labels("_corp")
+ assert "acme_corp" in results
+
+ # Test 10: Verify result ordering (should be by entity_id)
+ results = await storage.search_labels("tech", limit=10)
+ if len(results) > 1:
+ # Check if results are ordered alphabetically by entity_id
+ sorted_results = sorted(results)
+ assert results == sorted_results
+
+ # Test 11: Empty query string
+ results = await storage.search_labels("")
+ # Should return all nodes when query is empty (or handle gracefully)
+ assert isinstance(results, list)
+
+ # Test 12: Search with very large limit
+ results = await storage.search_labels("tech", limit=1000)
+ assert len(results) <= 5 # Can't exceed total number of nodes
+
+ print("✓ search_labels test passed")
+
+ async def test_deletion_operations(self, storage):
+ """Test node and edge deletion"""
+ # Create test data
+ await storage.upsert_node(
+ "delete_node_1",
+ {
+ "entity_id": "delete_node_1",
+ "entity_type": "Person",
+ "description": "Node to delete",
+ "source_id": "chunk_1",
+ },
+ )
+
+ await storage.upsert_node(
+ "delete_node_2",
+ {
+ "entity_id": "delete_node_2",
+ "entity_type": "Person",
+ "description": "Another node to delete",
+ "source_id": "chunk_1",
+ },
+ )
+
+ await storage.upsert_edge(
+ "delete_node_1",
+ "delete_node_2",
+ {"weight": 0.5, "description": "edge to delete", "source_id": "chunk_1"},
+ )
+
+ # Verify they exist
+ # assert await storage.has_node("delete_node_1") == True
+ # assert await storage.has_node("delete_node_2") == True
+ # assert await storage.has_edge("delete_node_1", "delete_node_2") == True
+ if await storage.has_node("delete_node_1") and await storage.has_node(
+ "delete_node_2"
+ ):
+ assert True
+ else:
+ assert False
+
+ if await storage.has_edge("delete_node_1", "delete_node_2"):
+ assert True
+ else:
+ assert False
+ # Delete edge
+ await storage.remove_edges([("delete_node_1", "delete_node_2")])
+ # assert await storage.has_edge("delete_node_1", "delete_node_2") == False
+ if not await storage.has_edge("delete_node_1", "delete_node_2"):
+ assert True
+ else:
+ assert False
+
+ # Delete nodes
+ await storage.remove_nodes(["delete_node_1", "delete_node_2"])
+ # assert await storage.has_node("delete_node_1") == False
+ # assert await storage.has_node("delete_node_2") == False
+ if not await storage.has_node("delete_node_1") and not await storage.has_node(
+ "delete_node_2"
+ ):
+ assert True
+ else:
+ assert False
+
+ async def test_knowledge_graph_retrieval(self, storage):
+ """Test comprehensive knowledge graph retrieval scenarios"""
+
+ # Test 1: Empty graph scenario
+ kg_empty = await storage.get_knowledge_graph(
+ "nonexistent_node", max_depth=2, max_nodes=10
+ )
+ assert isinstance(kg_empty, KnowledgeGraph)
+ assert len(kg_empty.nodes) == 0
+ assert len(kg_empty.edges) == 0
+ assert not kg_empty.is_truncated
+
+ # Test 2: Create a complex connected graph with multiple depths
+ # Layer 1: Central node
+ await storage.upsert_node(
+ "central_node",
+ {
+ "entity_id": "central_node",
+ "entity_type": "Hub",
+ "description": "Central hub node",
+ "source_id": "chunk_central",
+ "keywords": "hub,center",
+ },
+ )
+
+ # Layer 2: Direct neighbors
+ layer2_nodes = ["neighbor_a", "neighbor_b", "neighbor_c"]
+ for node_id in layer2_nodes:
+ await storage.upsert_node(
+ node_id,
+ {
+ "entity_id": node_id,
+ "entity_type": "Neighbor",
+ "description": f"Direct neighbor {node_id}",
+ "source_id": "chunk_layer2",
+ "keywords": f"neighbor,{node_id}",
+ },
+ )
+ # Connect to central node
+ await storage.upsert_edge(
+ "central_node",
+ node_id,
+ {
+ "weight": 0.8,
+ "description": f"connects to {node_id}",
+ "source_id": "chunk_central",
+ "keywords": "direct",
+ },
+ )
+
+ # Layer 3: Distant neighbors (only connected to layer 2)
+ layer3_nodes = ["distant_x", "distant_y"]
+ for i, node_id in enumerate(layer3_nodes):
+ await storage.upsert_node(
+ node_id,
+ {
+ "entity_id": node_id,
+ "entity_type": "Distant",
+ "description": f"Distant node {node_id}",
+ "source_id": "chunk_layer3",
+ "keywords": f"distant,{node_id}",
+ },
+ )
+ # Connect to one of the layer 2 nodes
+ await storage.upsert_edge(
+ layer2_nodes[i],
+ node_id,
+ {
+ "weight": 0.6,
+ "description": f"extends to {node_id}",
+ "source_id": "chunk_layer3",
+ "keywords": "distant",
+ },
+ )
+
+ # Layer 4: Very distant node (should be excluded with depth=2)
+ await storage.upsert_node(
+ "very_distant",
+ {
+ "entity_id": "very_distant",
+ "entity_type": "VeryDistant",
+ "description": "Very distant node",
+ "source_id": "chunk_layer4",
+ "keywords": "very,distant",
+ },
+ )
+ await storage.upsert_edge(
+ "distant_x",
+ "very_distant",
+ {
+ "weight": 0.3,
+ "description": "very far connection",
+ "source_id": "chunk_layer4",
+ "keywords": "very_distant",
+ },
+ )
+
+ # Isolated component (should not be included)
+ isolated_nodes = ["isolated_1", "isolated_2"]
+ for node_id in isolated_nodes:
+ await storage.upsert_node(
+ node_id,
+ {
+ "entity_id": node_id,
+ "entity_type": "Isolated",
+ "description": f"Isolated node {node_id}",
+ "source_id": "chunk_isolated",
+ "keywords": f"isolated,{node_id}",
+ },
+ )
+ await storage.upsert_edge(
+ "isolated_1",
+ "isolated_2",
+ {
+ "weight": 0.5,
+ "description": "isolated connection",
+ "source_id": "chunk_isolated",
+ "keywords": "isolated",
+ },
+ )
+
+ # Test 3: BFS traversal with depth limit
+ kg_depth1 = await storage.get_knowledge_graph(
+ "central_node", max_depth=1, max_nodes=10
+ )
+ assert isinstance(kg_depth1, KnowledgeGraph)
+
+ # Should include central node + direct neighbors (depth 1)
+ expected_nodes_depth1 = {
+ "central_node",
+ "neighbor_a",
+ "neighbor_b",
+ "neighbor_c",
+ }
+ actual_nodes_depth1 = {node.id for node in kg_depth1.nodes}
+ assert expected_nodes_depth1.issubset(actual_nodes_depth1)
+
+ # Should not include layer 3 nodes at depth 1
+ assert "distant_x" not in actual_nodes_depth1
+ assert "distant_y" not in actual_nodes_depth1
+
+ # Verify edge count for depth 1 (3 edges from central to neighbors)
+ assert len(kg_depth1.edges) >= 3
+
+ # Test 4: BFS traversal with depth 2
+ kg_depth2 = await storage.get_knowledge_graph(
+ "central_node", max_depth=2, max_nodes=20
+ )
+ assert isinstance(kg_depth2, KnowledgeGraph)
+
+ # Should include up to layer 3 nodes
+ expected_nodes_depth2 = {
+ "central_node",
+ "neighbor_a",
+ "neighbor_b",
+ "neighbor_c",
+ "distant_x",
+ "distant_y",
+ }
+ actual_nodes_depth2 = {node.id for node in kg_depth2.nodes}
+ assert expected_nodes_depth2.issubset(actual_nodes_depth2)
+
+ # Should not include very distant node at depth 2
+ assert "very_distant" not in actual_nodes_depth2
+
+ # Should not include isolated nodes
+ assert "isolated_1" not in actual_nodes_depth2
+ assert "isolated_2" not in actual_nodes_depth2
+
+ # Test 5: Node limit enforcement
+ kg_limited = await storage.get_knowledge_graph(
+ "central_node", max_depth=3, max_nodes=3
+ )
+ assert isinstance(kg_limited, KnowledgeGraph)
+ assert len(kg_limited.nodes) <= 3
+ # Should be truncated due to node limit
+ assert kg_limited.is_truncated
+
+ # Test 6: Verify node and edge properties are preserved
+ kg_full = await storage.get_knowledge_graph(
+ "central_node", max_depth=2, max_nodes=10
+ )
+
+ # Find central node and verify its properties
+ central_node_kg = next(
+ (node for node in kg_full.nodes if node.id == "central_node"), None
+ )
+ assert central_node_kg is not None
+ assert central_node_kg.properties["entity_type"] == "Hub"
+ assert central_node_kg.properties["description"] == "Central hub node"
+ assert central_node_kg.properties["source_id"] == "chunk_central"
+ assert central_node_kg.properties["keywords"] == "hub,center"
+
+ # Find an edge and verify its properties
+ central_to_neighbor_edge = next(
+ (
+ edge
+ for edge in kg_full.edges
+ if (edge.source == "central_node" and edge.target in layer2_nodes)
+ or (edge.target == "central_node" and edge.source in layer2_nodes)
+ ),
+ None,
+ )
+ assert central_to_neighbor_edge is not None
+ assert central_to_neighbor_edge.properties["weight"] == 0.8
+ assert "connects to" in central_to_neighbor_edge.properties["description"]
+ assert central_to_neighbor_edge.properties["source_id"] == "chunk_central"
+ assert central_to_neighbor_edge.properties["keywords"] == "direct"
+ assert central_to_neighbor_edge.type == "UNDIRECTED"
+
+ # Test 7: Wildcard retrieval ("*")
+ kg_wildcard = await storage.get_knowledge_graph("*", max_nodes=5)
+ assert isinstance(kg_wildcard, KnowledgeGraph)
+ assert len(kg_wildcard.nodes) <= 5
+ # Should return nodes with highest degree first
+ assert len(kg_wildcard.nodes) > 0
+
+ # The central node should be included as it has the highest degree
+ wildcard_node_ids = {node.id for node in kg_wildcard.nodes}
+ assert "central_node" in wildcard_node_ids
+
+ # Test 8: Edge uniqueness and bidirectionality handling
+ # Verify that bidirectional edges are represented as single undirected edges
+ kg_edge_test = await storage.get_knowledge_graph(
+ "central_node", max_depth=1, max_nodes=10
+ )
+ edge_pairs = set()
+ for edge in kg_edge_test.edges:
+ # Normalize edge representation
+ edge_pair = tuple(sorted([edge.source, edge.target]))
+ assert edge_pair not in edge_pairs, f"Duplicate edge found: {edge_pair}"
+ edge_pairs.add(edge_pair)
+
+ # Test 9: Different starting points should yield different but overlapping results
+ kg_from_neighbor = await storage.get_knowledge_graph(
+ "neighbor_a", max_depth=2, max_nodes=10
+ )
+ assert isinstance(kg_from_neighbor, KnowledgeGraph)
+
+ # Should include neighbor_a as starting point
+ neighbor_node_ids = {node.id for node in kg_from_neighbor.nodes}
+ assert "neighbor_a" in neighbor_node_ids
+
+ # Should reach central node and other neighbors through depth-2 traversal
+ assert "central_node" in neighbor_node_ids
+
+ # Test 10: Verify graph connectivity
+ # All returned nodes should be reachable from the starting node
+ def is_connected(nodes, edges, start_node_id):
+ """Verify all nodes are reachable from start_node via BFS"""
+ if not nodes:
+ return True
+
+ # Build adjacency list
+ adjacency = {node.id: set() for node in nodes}
+ for edge in edges:
+ adjacency[edge.source].add(edge.target)
+ adjacency[edge.target].add(edge.source)
+
+ # BFS to check connectivity
+ visited = set()
+ queue = [start_node_id]
+ visited.add(start_node_id)
+
+ while queue:
+ current = queue.pop(0)
+ for neighbor in adjacency.get(current, set()):
+ if neighbor not in visited:
+ visited.add(neighbor)
+ queue.append(neighbor)
+
+ node_ids = {node.id for node in nodes}
+ return visited == node_ids
+
+ kg_connectivity = await storage.get_knowledge_graph(
+ "central_node", max_depth=2, max_nodes=10
+ )
+ assert is_connected(
+ kg_connectivity.nodes, kg_connectivity.edges, "central_node"
+ )
+
+ print("✓ All knowledge graph retrieval tests passed comprehensively!")
+
+ async def test_drop_operation(self, storage):
+ """Test dropping all data"""
+ # Create some test data
+ await storage.upsert_node(
+ "drop_test_node",
+ {
+ "entity_id": "drop_test_node",
+ "entity_type": "TestNode",
+ "description": "Node for drop test",
+ "source_id": "chunk_1",
+ },
+ )
+
+ # Verify data exists
+ # assert await storage.has_node("drop_test_node") == True
+ if await storage.has_node("drop_test_node"):
+ assert True
+ else:
+ assert False
+
+ # Drop all data
+ result = await storage.drop()
+ assert result["status"] == "success"
+
+ # Verify data is gone
+ # assert await storage.has_node("drop_test_node") == False
+ if not await storage.has_node("drop_test_node"):
+ assert True
+ else:
+ assert False
+
+
+# Run the tests
+async def run_tests():
+ """Run all tests"""
+ test_instance = TestKuzuDBStorage()
+
+ # Create storage fixture
+ with tempfile.TemporaryDirectory() as temp_dir:
+ os.environ["KUZU_DB_PATH"] = os.path.join(temp_dir, "test.db")
+
+ # Mock embedding function
+ def embedding_func(x):
+ return [0.1, 0.2, 0.3]
+
+ storage = KuzuDBStorage(
+ namespace="test",
+ global_config={"max_graph_nodes": 1000},
+ embedding_func=embedding_func,
+ workspace="test_workspace",
+ )
+
+ await storage.initialize()
+
+ try:
+ print("Running KuzuDB tests...")
+
+ # Run tests
+ await test_instance.test_initialization(storage)
+ print("✓ Initialization test passed")
+
+ await test_instance.test_node_operations(storage)
+ print("✓ Node operations test passed")
+
+ await test_instance.test_edge_operations(storage)
+ print("✓ Edge operations test passed")
+
+ await test_instance.test_node_degree(storage)
+ print("✓ Node degree test passed")
+
+ await test_instance.test_get_node_edges(storage)
+ print("✓ Get node edges test passed")
+
+ await test_instance.test_batch_operations(storage)
+ print("✓ Batch operations test passed")
+
+ await test_instance.test_chunk_id_queries(storage)
+ print("✓ Chunk ID queries test passed")
+
+ await test_instance.test_get_all_labels(storage)
+ print("✓ Get all labels test passed")
+
+ await test_instance.test_get_all_nodes(storage)
+ print("✓ Get all nodes test passed")
+
+ await test_instance.test_get_all_edges(storage)
+ print("✓ Get all edges test passed")
+
+ await test_instance.test_get_popular_labels(storage)
+ print("✓ Get popular labels test passed")
+
+ await test_instance.test_search_labels(storage)
+ print("✓ Search labels test passed")
+
+ await test_instance.test_deletion_operations(storage)
+ print("✓ Deletion operations test passed")
+
+ await test_instance.test_knowledge_graph_retrieval(storage)
+ print("✓ Knowledge graph retrieval test passed")
+
+ await test_instance.test_drop_operation(storage)
+ print("✓ Drop operation test passed")
+
+ print("\nAll tests passed! ✅")
+
+ finally:
+ await storage.finalize()
+
+
+if __name__ == "__main__":
+ # Run the tests
+ asyncio.run(run_tests())
diff --git a/tests/test_kuzu_integration.py b/tests/test_kuzu_integration.py
new file mode 100644
index 0000000000..97c0a84218
--- /dev/null
+++ b/tests/test_kuzu_integration.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python
+"""
+Test script to verify KuzuDB integration with test_graph_storage.py
+"""
+
+import asyncio
+import os
+import sys
+import tempfile
+from pathlib import Path
+
+# Add project root to path
+sys.path.append(str(Path(__file__).parent.parent))
+
+from lightrag.kg.kuzu_impl import KuzuDBStorage
+from lightrag.types import KnowledgeGraph
+import numpy as np
+
+
+# Mock embedding function
+async def mock_embedding_func(texts):
+ return np.random.rand(len(texts), 10)
+
+
+async def test_kuzu_integration():
+ """Test basic KuzuDB integration functionality"""
+ # Create temporary directory for testing
+ with tempfile.TemporaryDirectory() as temp_dir:
+ # Set up KuzuDB environment
+ kuzu_db_path = os.path.join(temp_dir, "test_kuzu.db")
+ os.environ["KUZU_DB_PATH"] = kuzu_db_path
+ os.environ["KUZU_WORKSPACE"] = "test_workspace"
+
+ # Initialize KuzuDB storage
+ storage = KuzuDBStorage(
+ namespace="test_graph",
+ global_config={"max_graph_nodes": 1000},
+ embedding_func=mock_embedding_func,
+ workspace="test_workspace",
+ )
+
+ try:
+ # Initialize connection
+ await storage.initialize()
+ print("✓ KuzuDB initialization successful")
+
+ # Test basic node operations
+ node_id = "test_node"
+ node_data = {
+ "entity_id": node_id,
+ "entity_type": "Test",
+ "description": "A test node",
+ "source_id": "chunk_1",
+ }
+
+ await storage.upsert_node(node_id, node_data)
+ print("✓ Node insertion successful")
+
+ # Test node retrieval
+ retrieved_node = await storage.get_node(node_id)
+ assert retrieved_node is not None
+ assert retrieved_node["entity_id"] == node_id
+ print("✓ Node retrieval successful")
+
+ # Test edge operations
+ node2_id = "test_node_2"
+ node2_data = {
+ "entity_id": node2_id,
+ "entity_type": "Test",
+ "description": "Another test node",
+ "source_id": "chunk_1",
+ }
+
+ await storage.upsert_node(node2_id, node2_data)
+
+ edge_data = {
+ "weight": 0.8,
+ "description": "test relationship",
+ "keywords": "test",
+ "source_id": "chunk_1",
+ }
+
+ await storage.upsert_edge(node_id, node2_id, edge_data)
+ print("✓ Edge insertion successful")
+
+ # Test edge retrieval
+ retrieved_edge = await storage.get_edge(node_id, node2_id)
+ assert retrieved_edge is not None
+ assert retrieved_edge["weight"] == 0.8
+ print("✓ Edge retrieval successful")
+
+ # Test knowledge graph retrieval
+ kg = await storage.get_knowledge_graph(node_id, max_depth=2, max_nodes=10)
+ assert isinstance(kg, KnowledgeGraph)
+ assert len(kg.nodes) > 0
+ print("✓ Knowledge graph retrieval successful")
+
+ # Test cleanup
+ await storage.drop()
+ print("✓ Database cleanup successful")
+
+ print("\n🎉 All KuzuDB integration tests passed!")
+
+ finally:
+ await storage.finalize()
+
+
+if __name__ == "__main__":
+ asyncio.run(test_kuzu_integration())
diff --git a/tests/test_lightrag_ollama_chat.py b/tests/test_lightrag_ollama_chat.py
index 80038928f6..1d92b77468 100644
--- a/tests/test_lightrag_ollama_chat.py
+++ b/tests/test_lightrag_ollama_chat.py
@@ -13,6 +13,7 @@
import json
import argparse
import time
+import pytest
from typing import Dict, Any, Optional, List, Callable
from dataclasses import dataclass, asdict
from datetime import datetime
@@ -217,6 +218,29 @@ def get_base_url(endpoint: str = "chat") -> str:
return f"http://{server['host']}:{server['port']}/api/{endpoint}"
+def is_server_available() -> bool:
+ """Check if the Ollama server is available"""
+ try:
+ url = get_base_url("chat")
+ response = requests.post(
+ url, json={"model": "test", "messages": [], "stream": False}, timeout=5
+ )
+ if response.status_code == 200:
+ return True
+ else:
+ print(f"Server returned status code {response.status_code}")
+ return False
+ except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
+ return False
+
+
+# Pytest marker for tests that require server
+requires_server = pytest.mark.skipif(
+ not is_server_available(),
+ reason="Ollama server not available at configured host:port",
+)
+
+
def create_chat_request_data(
content: str,
stream: bool = False,
@@ -293,6 +317,7 @@ def run_test(func: Callable, name: str) -> None:
raise
+@requires_server
def test_non_stream_chat() -> None:
"""Test non-streaming call to /api/chat endpoint"""
url = get_base_url()
@@ -317,6 +342,7 @@ def test_non_stream_chat() -> None:
)
+@requires_server
def test_stream_chat() -> None:
"""Test streaming call to /api/chat endpoint
@@ -377,6 +403,7 @@ def test_stream_chat() -> None:
print()
+@requires_server
def test_query_modes() -> None:
"""Test different query mode prefixes
@@ -436,6 +463,7 @@ def create_error_test_data(error_type: str) -> Dict[str, Any]:
return error_data.get(error_type, error_data["empty_messages"])
+@requires_server
def test_stream_error_handling() -> None:
"""Test error handling for streaming responses
@@ -482,6 +510,7 @@ def test_stream_error_handling() -> None:
response.close()
+@requires_server
def test_error_handling() -> None:
"""Test error handling for non-streaming responses
@@ -529,6 +558,7 @@ def test_error_handling() -> None:
print_json_response(response.json(), "Error message")
+@requires_server
def test_non_stream_generate() -> None:
"""Test non-streaming call to /api/generate endpoint"""
url = get_base_url("generate")
@@ -548,6 +578,7 @@ def test_non_stream_generate() -> None:
print(json.dumps(response_json, ensure_ascii=False, indent=2))
+@requires_server
def test_stream_generate() -> None:
"""Test streaming call to /api/generate endpoint"""
url = get_base_url("generate")
@@ -588,6 +619,7 @@ def test_stream_generate() -> None:
print()
+@requires_server
def test_generate_with_system() -> None:
"""Test generate with system prompt"""
url = get_base_url("generate")
@@ -616,6 +648,7 @@ def test_generate_with_system() -> None:
)
+@requires_server
def test_generate_error_handling() -> None:
"""Test error handling for generate endpoint"""
url = get_base_url("generate")
@@ -641,6 +674,7 @@ def test_generate_error_handling() -> None:
print_json_response(response.json(), "Error message")
+@requires_server
def test_generate_concurrent() -> None:
"""Test concurrent generate requests"""
import asyncio
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000000..c98c5a873b
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,2196 @@
+version = 1
+revision = 1
+requires-python = ">=3.10"
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version < '3.11'",
+]
+
+[[package]]
+name = "aiofiles"
+version = "24.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896 },
+]
+
+[[package]]
+name = "aiohappyeyeballs"
+version = "2.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 },
+]
+
+[[package]]
+name = "aiohttp"
+version = "3.12.13"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohappyeyeballs" },
+ { name = "aiosignal" },
+ { name = "async-timeout", marker = "python_full_version < '3.11'" },
+ { name = "attrs" },
+ { name = "frozenlist" },
+ { name = "multidict" },
+ { name = "propcache" },
+ { name = "yarl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/6e/ab88e7cb2a4058bed2f7870276454f85a7c56cd6da79349eb314fc7bbcaa/aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce", size = 7819160 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/2d/27e4347660723738b01daa3f5769d56170f232bf4695dd4613340da135bb/aiohttp-3.12.13-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5421af8f22a98f640261ee48aae3a37f0c41371e99412d55eaf2f8a46d5dad29", size = 702090 },
+ { url = "https://files.pythonhosted.org/packages/10/0b/4a8e0468ee8f2b9aff3c05f2c3a6be1dfc40b03f68a91b31041d798a9510/aiohttp-3.12.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fcda86f6cb318ba36ed8f1396a6a4a3fd8f856f84d426584392083d10da4de0", size = 478440 },
+ { url = "https://files.pythonhosted.org/packages/b9/c8/2086df2f9a842b13feb92d071edf756be89250f404f10966b7bc28317f17/aiohttp-3.12.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cd71c9fb92aceb5a23c4c39d8ecc80389c178eba9feab77f19274843eb9412d", size = 466215 },
+ { url = "https://files.pythonhosted.org/packages/a7/3d/d23e5bd978bc8012a65853959b13bd3b55c6e5afc172d89c26ad6624c52b/aiohttp-3.12.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34ebf1aca12845066c963016655dac897651e1544f22a34c9b461ac3b4b1d3aa", size = 1648271 },
+ { url = "https://files.pythonhosted.org/packages/31/31/e00122447bb137591c202786062f26dd383574c9f5157144127077d5733e/aiohttp-3.12.13-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:893a4639694c5b7edd4bdd8141be296042b6806e27cc1d794e585c43010cc294", size = 1622329 },
+ { url = "https://files.pythonhosted.org/packages/04/01/caef70be3ac38986969045f21f5fb802ce517b3f371f0615206bf8aa6423/aiohttp-3.12.13-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:663d8ee3ffb3494502ebcccb49078faddbb84c1d870f9c1dd5a29e85d1f747ce", size = 1694734 },
+ { url = "https://files.pythonhosted.org/packages/3f/15/328b71fedecf69a9fd2306549b11c8966e420648a3938d75d3ed5bcb47f6/aiohttp-3.12.13-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0f8f6a85a0006ae2709aa4ce05749ba2cdcb4b43d6c21a16c8517c16593aabe", size = 1737049 },
+ { url = "https://files.pythonhosted.org/packages/e6/7a/d85866a642158e1147c7da5f93ad66b07e5452a84ec4258e5f06b9071e92/aiohttp-3.12.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1582745eb63df267c92d8b61ca655a0ce62105ef62542c00a74590f306be8cb5", size = 1641715 },
+ { url = "https://files.pythonhosted.org/packages/14/57/3588800d5d2f5f3e1cb6e7a72747d1abc1e67ba5048e8b845183259c2e9b/aiohttp-3.12.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d59227776ee2aa64226f7e086638baa645f4b044f2947dbf85c76ab11dcba073", size = 1581836 },
+ { url = "https://files.pythonhosted.org/packages/2f/55/c913332899a916d85781aa74572f60fd98127449b156ad9c19e23135b0e4/aiohttp-3.12.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06b07c418bde1c8e737d8fa67741072bd3f5b0fb66cf8c0655172188c17e5fa6", size = 1625685 },
+ { url = "https://files.pythonhosted.org/packages/4c/34/26cded195f3bff128d6a6d58d7a0be2ae7d001ea029e0fe9008dcdc6a009/aiohttp-3.12.13-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9445c1842680efac0f81d272fd8db7163acfcc2b1436e3f420f4c9a9c5a50795", size = 1636471 },
+ { url = "https://files.pythonhosted.org/packages/19/21/70629ca006820fccbcec07f3cd5966cbd966e2d853d6da55339af85555b9/aiohttp-3.12.13-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:09c4767af0b0b98c724f5d47f2bf33395c8986995b0a9dab0575ca81a554a8c0", size = 1611923 },
+ { url = "https://files.pythonhosted.org/packages/31/80/7fa3f3bebf533aa6ae6508b51ac0de9965e88f9654fa679cc1a29d335a79/aiohttp-3.12.13-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f3854fbde7a465318ad8d3fc5bef8f059e6d0a87e71a0d3360bb56c0bf87b18a", size = 1691511 },
+ { url = "https://files.pythonhosted.org/packages/0f/7a/359974653a3cdd3e9cee8ca10072a662c3c0eb46a359c6a1f667b0296e2f/aiohttp-3.12.13-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2332b4c361c05ecd381edb99e2a33733f3db906739a83a483974b3df70a51b40", size = 1714751 },
+ { url = "https://files.pythonhosted.org/packages/2d/24/0aa03d522171ce19064347afeefadb008be31ace0bbb7d44ceb055700a14/aiohttp-3.12.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1561db63fa1b658cd94325d303933553ea7d89ae09ff21cc3bcd41b8521fbbb6", size = 1643090 },
+ { url = "https://files.pythonhosted.org/packages/86/2e/7d4b0026a41e4b467e143221c51b279083b7044a4b104054f5c6464082ff/aiohttp-3.12.13-cp310-cp310-win32.whl", hash = "sha256:a0be857f0b35177ba09d7c472825d1b711d11c6d0e8a2052804e3b93166de1ad", size = 427526 },
+ { url = "https://files.pythonhosted.org/packages/17/de/34d998da1e7f0de86382160d039131e9b0af1962eebfe53dda2b61d250e7/aiohttp-3.12.13-cp310-cp310-win_amd64.whl", hash = "sha256:fcc30ad4fb5cb41a33953292d45f54ef4066746d625992aeac33b8c681173178", size = 450734 },
+ { url = "https://files.pythonhosted.org/packages/6a/65/5566b49553bf20ffed6041c665a5504fb047cefdef1b701407b8ce1a47c4/aiohttp-3.12.13-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c229b1437aa2576b99384e4be668af1db84b31a45305d02f61f5497cfa6f60c", size = 709401 },
+ { url = "https://files.pythonhosted.org/packages/14/b5/48e4cc61b54850bdfafa8fe0b641ab35ad53d8e5a65ab22b310e0902fa42/aiohttp-3.12.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04076d8c63471e51e3689c93940775dc3d12d855c0c80d18ac5a1c68f0904358", size = 481669 },
+ { url = "https://files.pythonhosted.org/packages/04/4f/e3f95c8b2a20a0437d51d41d5ccc4a02970d8ad59352efb43ea2841bd08e/aiohttp-3.12.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55683615813ce3601640cfaa1041174dc956d28ba0511c8cbd75273eb0587014", size = 469933 },
+ { url = "https://files.pythonhosted.org/packages/41/c9/c5269f3b6453b1cfbd2cfbb6a777d718c5f086a3727f576c51a468b03ae2/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:921bc91e602d7506d37643e77819cb0b840d4ebb5f8d6408423af3d3bf79a7b7", size = 1740128 },
+ { url = "https://files.pythonhosted.org/packages/6f/49/a3f76caa62773d33d0cfaa842bdf5789a78749dbfe697df38ab1badff369/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e72d17fe0974ddeae8ed86db297e23dba39c7ac36d84acdbb53df2e18505a013", size = 1688796 },
+ { url = "https://files.pythonhosted.org/packages/ad/e4/556fccc4576dc22bf18554b64cc873b1a3e5429a5bdb7bbef7f5d0bc7664/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0653d15587909a52e024a261943cf1c5bdc69acb71f411b0dd5966d065a51a47", size = 1787589 },
+ { url = "https://files.pythonhosted.org/packages/b9/3d/d81b13ed48e1a46734f848e26d55a7391708421a80336e341d2aef3b6db2/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a77b48997c66722c65e157c06c74332cdf9c7ad00494b85ec43f324e5c5a9b9a", size = 1826635 },
+ { url = "https://files.pythonhosted.org/packages/75/a5/472e25f347da88459188cdaadd1f108f6292f8a25e62d226e63f860486d1/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6946bae55fd36cfb8e4092c921075cde029c71c7cb571d72f1079d1e4e013bc", size = 1729095 },
+ { url = "https://files.pythonhosted.org/packages/b9/fe/322a78b9ac1725bfc59dfc301a5342e73d817592828e4445bd8f4ff83489/aiohttp-3.12.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f95db8c8b219bcf294a53742c7bda49b80ceb9d577c8e7aa075612b7f39ffb7", size = 1666170 },
+ { url = "https://files.pythonhosted.org/packages/7a/77/ec80912270e231d5e3839dbd6c065472b9920a159ec8a1895cf868c2708e/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03d5eb3cfb4949ab4c74822fb3326cd9655c2b9fe22e4257e2100d44215b2e2b", size = 1714444 },
+ { url = "https://files.pythonhosted.org/packages/21/b2/fb5aedbcb2b58d4180e58500e7c23ff8593258c27c089abfbcc7db65bd40/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6383dd0ffa15515283c26cbf41ac8e6705aab54b4cbb77bdb8935a713a89bee9", size = 1709604 },
+ { url = "https://files.pythonhosted.org/packages/e3/15/a94c05f7c4dc8904f80b6001ad6e07e035c58a8ebfcc15e6b5d58500c858/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6548a411bc8219b45ba2577716493aa63b12803d1e5dc70508c539d0db8dbf5a", size = 1689786 },
+ { url = "https://files.pythonhosted.org/packages/1d/fd/0d2e618388f7a7a4441eed578b626bda9ec6b5361cd2954cfc5ab39aa170/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:81b0fcbfe59a4ca41dc8f635c2a4a71e63f75168cc91026c61be665945739e2d", size = 1783389 },
+ { url = "https://files.pythonhosted.org/packages/a6/6b/6986d0c75996ef7e64ff7619b9b7449b1d1cbbe05c6755e65d92f1784fe9/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6a83797a0174e7995e5edce9dcecc517c642eb43bc3cba296d4512edf346eee2", size = 1803853 },
+ { url = "https://files.pythonhosted.org/packages/21/65/cd37b38f6655d95dd07d496b6d2f3924f579c43fd64b0e32b547b9c24df5/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5734d8469a5633a4e9ffdf9983ff7cdb512524645c7a3d4bc8a3de45b935ac3", size = 1716909 },
+ { url = "https://files.pythonhosted.org/packages/fd/20/2de7012427dc116714c38ca564467f6143aec3d5eca3768848d62aa43e62/aiohttp-3.12.13-cp311-cp311-win32.whl", hash = "sha256:fef8d50dfa482925bb6b4c208b40d8e9fa54cecba923dc65b825a72eed9a5dbd", size = 427036 },
+ { url = "https://files.pythonhosted.org/packages/f8/b6/98518bcc615ef998a64bef371178b9afc98ee25895b4f476c428fade2220/aiohttp-3.12.13-cp311-cp311-win_amd64.whl", hash = "sha256:9a27da9c3b5ed9d04c36ad2df65b38a96a37e9cfba6f1381b842d05d98e6afe9", size = 451427 },
+ { url = "https://files.pythonhosted.org/packages/b4/6a/ce40e329788013cd190b1d62bbabb2b6a9673ecb6d836298635b939562ef/aiohttp-3.12.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0aa580cf80558557285b49452151b9c69f2fa3ad94c5c9e76e684719a8791b73", size = 700491 },
+ { url = "https://files.pythonhosted.org/packages/28/d9/7150d5cf9163e05081f1c5c64a0cdf3c32d2f56e2ac95db2a28fe90eca69/aiohttp-3.12.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b103a7e414b57e6939cc4dece8e282cfb22043efd0c7298044f6594cf83ab347", size = 475104 },
+ { url = "https://files.pythonhosted.org/packages/f8/91/d42ba4aed039ce6e449b3e2db694328756c152a79804e64e3da5bc19dffc/aiohttp-3.12.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f64e748e9e741d2eccff9597d09fb3cd962210e5b5716047cbb646dc8fe06f", size = 467948 },
+ { url = "https://files.pythonhosted.org/packages/99/3b/06f0a632775946981d7c4e5a865cddb6e8dfdbaed2f56f9ade7bb4a1039b/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c955989bf4c696d2ededc6b0ccb85a73623ae6e112439398935362bacfaaf6", size = 1714742 },
+ { url = "https://files.pythonhosted.org/packages/92/a6/2552eebad9ec5e3581a89256276009e6a974dc0793632796af144df8b740/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d640191016763fab76072c87d8854a19e8e65d7a6fcfcbf017926bdbbb30a7e5", size = 1697393 },
+ { url = "https://files.pythonhosted.org/packages/d8/9f/bd08fdde114b3fec7a021381b537b21920cdd2aa29ad48c5dffd8ee314f1/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dc507481266b410dede95dd9f26c8d6f5a14315372cc48a6e43eac652237d9b", size = 1752486 },
+ { url = "https://files.pythonhosted.org/packages/f7/e1/affdea8723aec5bd0959171b5490dccd9a91fcc505c8c26c9f1dca73474d/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a94daa873465d518db073bd95d75f14302e0208a08e8c942b2f3f1c07288a75", size = 1798643 },
+ { url = "https://files.pythonhosted.org/packages/f3/9d/666d856cc3af3a62ae86393baa3074cc1d591a47d89dc3bf16f6eb2c8d32/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f52420cde4ce0bb9425a375d95577fe082cb5721ecb61da3049b55189e4e6", size = 1718082 },
+ { url = "https://files.pythonhosted.org/packages/f3/ce/3c185293843d17be063dada45efd2712bb6bf6370b37104b4eda908ffdbd/aiohttp-3.12.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7df1f620ec40f1a7fbcb99ea17d7326ea6996715e78f71a1c9a021e31b96b8", size = 1633884 },
+ { url = "https://files.pythonhosted.org/packages/3a/5b/f3413f4b238113be35dfd6794e65029250d4b93caa0974ca572217745bdb/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3062d4ad53b36e17796dce1c0d6da0ad27a015c321e663657ba1cc7659cfc710", size = 1694943 },
+ { url = "https://files.pythonhosted.org/packages/82/c8/0e56e8bf12081faca85d14a6929ad5c1263c146149cd66caa7bc12255b6d/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8605e22d2a86b8e51ffb5253d9045ea73683d92d47c0b1438e11a359bdb94462", size = 1716398 },
+ { url = "https://files.pythonhosted.org/packages/ea/f3/33192b4761f7f9b2f7f4281365d925d663629cfaea093a64b658b94fc8e1/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54fbbe6beafc2820de71ece2198458a711e224e116efefa01b7969f3e2b3ddae", size = 1657051 },
+ { url = "https://files.pythonhosted.org/packages/5e/0b/26ddd91ca8f84c48452431cb4c5dd9523b13bc0c9766bda468e072ac9e29/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:050bd277dfc3768b606fd4eae79dd58ceda67d8b0b3c565656a89ae34525d15e", size = 1736611 },
+ { url = "https://files.pythonhosted.org/packages/c3/8d/e04569aae853302648e2c138a680a6a2f02e374c5b6711732b29f1e129cc/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2637a60910b58f50f22379b6797466c3aa6ae28a6ab6404e09175ce4955b4e6a", size = 1764586 },
+ { url = "https://files.pythonhosted.org/packages/ac/98/c193c1d1198571d988454e4ed75adc21c55af247a9fda08236602921c8c8/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e986067357550d1aaa21cfe9897fa19e680110551518a5a7cf44e6c5638cb8b5", size = 1724197 },
+ { url = "https://files.pythonhosted.org/packages/e7/9e/07bb8aa11eec762c6b1ff61575eeeb2657df11ab3d3abfa528d95f3e9337/aiohttp-3.12.13-cp312-cp312-win32.whl", hash = "sha256:ac941a80aeea2aaae2875c9500861a3ba356f9ff17b9cb2dbfb5cbf91baaf5bf", size = 421771 },
+ { url = "https://files.pythonhosted.org/packages/52/66/3ce877e56ec0813069cdc9607cd979575859c597b6fb9b4182c6d5f31886/aiohttp-3.12.13-cp312-cp312-win_amd64.whl", hash = "sha256:671f41e6146a749b6c81cb7fd07f5a8356d46febdaaaf07b0e774ff04830461e", size = 447869 },
+ { url = "https://files.pythonhosted.org/packages/11/0f/db19abdf2d86aa1deec3c1e0e5ea46a587b97c07a16516b6438428b3a3f8/aiohttp-3.12.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d4a18e61f271127465bdb0e8ff36e8f02ac4a32a80d8927aa52371e93cd87938", size = 694910 },
+ { url = "https://files.pythonhosted.org/packages/d5/81/0ab551e1b5d7f1339e2d6eb482456ccbe9025605b28eed2b1c0203aaaade/aiohttp-3.12.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:532542cb48691179455fab429cdb0d558b5e5290b033b87478f2aa6af5d20ace", size = 472566 },
+ { url = "https://files.pythonhosted.org/packages/34/3f/6b7d336663337672d29b1f82d1f252ec1a040fe2d548f709d3f90fa2218a/aiohttp-3.12.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d7eea18b52f23c050ae9db5d01f3d264ab08f09e7356d6f68e3f3ac2de9dfabb", size = 464856 },
+ { url = "https://files.pythonhosted.org/packages/26/7f/32ca0f170496aa2ab9b812630fac0c2372c531b797e1deb3deb4cea904bd/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad7c8e5c25f2a26842a7c239de3f7b6bfb92304593ef997c04ac49fb703ff4d7", size = 1703683 },
+ { url = "https://files.pythonhosted.org/packages/ec/53/d5513624b33a811c0abea8461e30a732294112318276ce3dbf047dbd9d8b/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6af355b483e3fe9d7336d84539fef460120c2f6e50e06c658fe2907c69262d6b", size = 1684946 },
+ { url = "https://files.pythonhosted.org/packages/37/72/4c237dd127827b0247dc138d3ebd49c2ded6114c6991bbe969058575f25f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a95cf9f097498f35c88e3609f55bb47b28a5ef67f6888f4390b3d73e2bac6177", size = 1737017 },
+ { url = "https://files.pythonhosted.org/packages/0d/67/8a7eb3afa01e9d0acc26e1ef847c1a9111f8b42b82955fcd9faeb84edeb4/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8ed8c38a1c584fe99a475a8f60eefc0b682ea413a84c6ce769bb19a7ff1c5ef", size = 1786390 },
+ { url = "https://files.pythonhosted.org/packages/48/19/0377df97dd0176ad23cd8cad4fd4232cfeadcec6c1b7f036315305c98e3f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0b9170d5d800126b5bc89d3053a2363406d6e327afb6afaeda2d19ee8bb103", size = 1708719 },
+ { url = "https://files.pythonhosted.org/packages/61/97/ade1982a5c642b45f3622255173e40c3eed289c169f89d00eeac29a89906/aiohttp-3.12.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:372feeace612ef8eb41f05ae014a92121a512bd5067db8f25101dd88a8db11da", size = 1622424 },
+ { url = "https://files.pythonhosted.org/packages/99/ab/00ad3eea004e1d07ccc406e44cfe2b8da5acb72f8c66aeeb11a096798868/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a946d3702f7965d81f7af7ea8fb03bb33fe53d311df48a46eeca17e9e0beed2d", size = 1675447 },
+ { url = "https://files.pythonhosted.org/packages/3f/fe/74e5ce8b2ccaba445fe0087abc201bfd7259431d92ae608f684fcac5d143/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a0c4725fae86555bbb1d4082129e21de7264f4ab14baf735278c974785cd2041", size = 1707110 },
+ { url = "https://files.pythonhosted.org/packages/ef/c4/39af17807f694f7a267bd8ab1fbacf16ad66740862192a6c8abac2bff813/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b28ea2f708234f0a5c44eb6c7d9eb63a148ce3252ba0140d050b091b6e842d1", size = 1649706 },
+ { url = "https://files.pythonhosted.org/packages/38/e8/f5a0a5f44f19f171d8477059aa5f28a158d7d57fe1a46c553e231f698435/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d4f5becd2a5791829f79608c6f3dc745388162376f310eb9c142c985f9441cc1", size = 1725839 },
+ { url = "https://files.pythonhosted.org/packages/fd/ac/81acc594c7f529ef4419d3866913f628cd4fa9cab17f7bf410a5c3c04c53/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:60f2ce6b944e97649051d5f5cc0f439360690b73909230e107fd45a359d3e911", size = 1759311 },
+ { url = "https://files.pythonhosted.org/packages/38/0d/aabe636bd25c6ab7b18825e5a97d40024da75152bec39aa6ac8b7a677630/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69fc1909857401b67bf599c793f2183fbc4804717388b0b888f27f9929aa41f3", size = 1708202 },
+ { url = "https://files.pythonhosted.org/packages/1f/ab/561ef2d8a223261683fb95a6283ad0d36cb66c87503f3a7dde7afe208bb2/aiohttp-3.12.13-cp313-cp313-win32.whl", hash = "sha256:7d7e68787a2046b0e44ba5587aa723ce05d711e3a3665b6b7545328ac8e3c0dd", size = 420794 },
+ { url = "https://files.pythonhosted.org/packages/9d/47/b11d0089875a23bff0abd3edb5516bcd454db3fefab8604f5e4b07bd6210/aiohttp-3.12.13-cp313-cp313-win_amd64.whl", hash = "sha256:5a178390ca90419bfd41419a809688c368e63c86bd725e1186dd97f6b89c2706", size = 446735 },
+]
+
+[[package]]
+name = "aiosignal"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "frozenlist" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490 },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
+]
+
+[[package]]
+name = "anyio"
+version = "4.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "sniffio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 },
+]
+
+[[package]]
+name = "ascii-colors"
+version = "0.11.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "wcwidth" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3f/2d/399ec9c4bd76aa62321b6d024003fce3209d0dfcb3676f85cc7e2d4c57fc/ascii_colors-0.11.4.tar.gz", hash = "sha256:b308949c2ada24b6cda89aa9c3c1dba3ed324ab45dc516ea5f6dc77fd3aa4220", size = 106608 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/b6/6b1dc0962d3210256e5da600a60939ce447eb9f0788273d8642a0c2f7498/ascii_colors-0.11.4-py3-none-any.whl", hash = "sha256:1ffd62a0bfb2d51a8ab942f0844fe6b7c11aaa04d19bd6e50ff149b38c9738a6", size = 71383 },
+]
+
+[[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 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 },
+]
+
+[[package]]
+name = "asyncpg"
+version = "0.30.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "async-timeout", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bb/07/1650a8c30e3a5c625478fa8aafd89a8dd7d85999bf7169b16f54973ebf2c/asyncpg-0.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e", size = 673143 },
+ { url = "https://files.pythonhosted.org/packages/a0/9a/568ff9b590d0954553c56806766914c149609b828c426c5118d4869111d3/asyncpg-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0", size = 645035 },
+ { url = "https://files.pythonhosted.org/packages/de/11/6f2fa6c902f341ca10403743701ea952bca896fc5b07cc1f4705d2bb0593/asyncpg-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3152fef2e265c9c24eec4ee3d22b4f4d2703d30614b0b6753e9ed4115c8a146f", size = 2912384 },
+ { url = "https://files.pythonhosted.org/packages/83/83/44bd393919c504ffe4a82d0aed8ea0e55eb1571a1dea6a4922b723f0a03b/asyncpg-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7255812ac85099a0e1ffb81b10dc477b9973345793776b128a23e60148dd1af", size = 2947526 },
+ { url = "https://files.pythonhosted.org/packages/08/85/e23dd3a2b55536eb0ded80c457b0693352262dc70426ef4d4a6fc994fa51/asyncpg-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:578445f09f45d1ad7abddbff2a3c7f7c291738fdae0abffbeb737d3fc3ab8b75", size = 2895390 },
+ { url = "https://files.pythonhosted.org/packages/9b/26/fa96c8f4877d47dc6c1864fef5500b446522365da3d3d0ee89a5cce71a3f/asyncpg-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c42f6bb65a277ce4d93f3fba46b91a265631c8df7250592dd4f11f8b0152150f", size = 3015630 },
+ { url = "https://files.pythonhosted.org/packages/34/00/814514eb9287614188a5179a8b6e588a3611ca47d41937af0f3a844b1b4b/asyncpg-0.30.0-cp310-cp310-win32.whl", hash = "sha256:aa403147d3e07a267ada2ae34dfc9324e67ccc4cdca35261c8c22792ba2b10cf", size = 568760 },
+ { url = "https://files.pythonhosted.org/packages/f0/28/869a7a279400f8b06dd237266fdd7220bc5f7c975348fea5d1e6909588e9/asyncpg-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb622c94db4e13137c4c7f98834185049cc50ee01d8f657ef898b6407c7b9c50", size = 625764 },
+ { url = "https://files.pythonhosted.org/packages/4c/0e/f5d708add0d0b97446c402db7e8dd4c4183c13edaabe8a8500b411e7b495/asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a", size = 674506 },
+ { url = "https://files.pythonhosted.org/packages/6a/a0/67ec9a75cb24a1d99f97b8437c8d56da40e6f6bd23b04e2f4ea5d5ad82ac/asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed", size = 645922 },
+ { url = "https://files.pythonhosted.org/packages/5c/d9/a7584f24174bd86ff1053b14bb841f9e714380c672f61c906eb01d8ec433/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a", size = 3079565 },
+ { url = "https://files.pythonhosted.org/packages/a0/d7/a4c0f9660e333114bdb04d1a9ac70db690dd4ae003f34f691139a5cbdae3/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956", size = 3109962 },
+ { url = "https://files.pythonhosted.org/packages/3c/21/199fd16b5a981b1575923cbb5d9cf916fdc936b377e0423099f209e7e73d/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056", size = 3064791 },
+ { url = "https://files.pythonhosted.org/packages/77/52/0004809b3427534a0c9139c08c87b515f1c77a8376a50ae29f001e53962f/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454", size = 3188696 },
+ { url = "https://files.pythonhosted.org/packages/52/cb/fbad941cd466117be58b774a3f1cc9ecc659af625f028b163b1e646a55fe/asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d", size = 567358 },
+ { url = "https://files.pythonhosted.org/packages/3c/0a/0a32307cf166d50e1ad120d9b81a33a948a1a5463ebfa5a96cc5606c0863/asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f", size = 629375 },
+ { url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162 },
+ { url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025 },
+ { url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243 },
+ { url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059 },
+ { url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596 },
+ { url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632 },
+ { url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186 },
+ { url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064 },
+ { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373 },
+ { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745 },
+ { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103 },
+ { url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471 },
+ { url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253 },
+ { url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720 },
+ { url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404 },
+ { url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623 },
+]
+
+[[package]]
+name = "attrs"
+version = "25.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 },
+]
+
+[[package]]
+name = "bcrypt"
+version = "4.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719 },
+ { url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001 },
+ { url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451 },
+ { url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792 },
+ { url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752 },
+ { url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762 },
+ { url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384 },
+ { url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329 },
+ { url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241 },
+ { url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617 },
+ { url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751 },
+ { url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965 },
+ { url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316 },
+ { url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752 },
+ { url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019 },
+ { url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174 },
+ { url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870 },
+ { url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601 },
+ { url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660 },
+ { url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083 },
+ { url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237 },
+ { url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737 },
+ { url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741 },
+ { url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472 },
+ { url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606 },
+ { url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867 },
+ { url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589 },
+ { url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794 },
+ { url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969 },
+ { url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158 },
+ { url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285 },
+ { url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583 },
+ { url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896 },
+ { url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492 },
+ { url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213 },
+ { url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162 },
+ { url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856 },
+ { url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726 },
+ { url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664 },
+ { url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128 },
+ { url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598 },
+ { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799 },
+ { url = "https://files.pythonhosted.org/packages/55/2d/0c7e5ab0524bf1a443e34cdd3926ec6f5879889b2f3c32b2f5074e99ed53/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1", size = 275367 },
+ { url = "https://files.pythonhosted.org/packages/10/4f/f77509f08bdff8806ecc4dc472b6e187c946c730565a7470db772d25df70/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d", size = 280644 },
+ { url = "https://files.pythonhosted.org/packages/35/18/7d9dc16a3a4d530d0a9b845160e9e5d8eb4f00483e05d44bb4116a1861da/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492", size = 274881 },
+ { url = "https://files.pythonhosted.org/packages/df/c4/ae6921088adf1e37f2a3a6a688e72e7d9e45fdd3ae5e0bc931870c1ebbda/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90", size = 280203 },
+ { url = "https://files.pythonhosted.org/packages/4c/b1/1289e21d710496b88340369137cc4c5f6ee036401190ea116a7b4ae6d32a/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a", size = 275103 },
+ { url = "https://files.pythonhosted.org/packages/94/41/19be9fe17e4ffc5d10b7b67f10e459fc4eee6ffe9056a88de511920cfd8d/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce", size = 280513 },
+ { url = "https://files.pythonhosted.org/packages/aa/73/05687a9ef89edebdd8ad7474c16d8af685eb4591c3c38300bb6aad4f0076/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8", size = 274685 },
+ { url = "https://files.pythonhosted.org/packages/63/13/47bba97924ebe86a62ef83dc75b7c8a881d53c535f83e2c54c4bd701e05c/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938", size = 280110 },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.7.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/de/8a/c729b6b60c66a38f590c4e774decc4b2ec7b0576be8f1aa984a53ffa812a/certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079", size = 160386 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/66/f3/80a3f974c8b535d394ff960a11ac20368e06b736da395b551a49ce950cce/certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39", size = 159230 },
+]
+
+[[package]]
+name = "cffi"
+version = "1.17.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 },
+ { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 },
+ { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 },
+ { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 },
+ { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 },
+ { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 },
+ { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 },
+ { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 },
+ { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 },
+ { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 },
+ { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 },
+ { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 },
+ { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 },
+ { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 },
+ { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 },
+ { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 },
+ { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 },
+ { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 },
+ { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 },
+ { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 },
+ { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 },
+ { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 },
+ { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 },
+ { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 },
+ { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 },
+ { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 },
+ { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 },
+ { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 },
+ { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 },
+ { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 },
+ { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 },
+ { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 },
+ { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 },
+ { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 },
+ { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 },
+ { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
+ { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
+ { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
+ { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
+ { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
+ { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
+ { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
+ { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
+ { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
+ { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
+ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
+]
+
+[[package]]
+name = "cfgv"
+version = "3.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818 },
+ { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649 },
+ { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045 },
+ { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356 },
+ { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471 },
+ { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317 },
+ { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368 },
+ { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491 },
+ { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695 },
+ { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849 },
+ { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091 },
+ { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445 },
+ { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782 },
+ { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 },
+ { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 },
+ { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 },
+ { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 },
+ { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 },
+ { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 },
+ { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 },
+ { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 },
+ { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 },
+ { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 },
+ { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 },
+ { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 },
+ { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 },
+ { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 },
+ { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 },
+ { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 },
+ { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 },
+ { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 },
+ { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 },
+ { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 },
+ { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 },
+ { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 },
+ { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 },
+ { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 },
+ { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 },
+ { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 },
+ { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 },
+ { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 },
+ { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 },
+ { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 },
+ { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 },
+ { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 },
+ { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 },
+ { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 },
+ { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 },
+ { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 },
+ { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 },
+ { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 },
+ { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 },
+ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 },
+]
+
+[[package]]
+name = "click"
+version = "8.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
+]
+
+[[package]]
+name = "configparser"
+version = "7.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8b/ac/ea19242153b5e8be412a726a70e82c7b5c1537c83f61b20995b2eda3dcd7/configparser-7.2.0.tar.gz", hash = "sha256:b629cc8ae916e3afbd36d1b3d093f34193d851e11998920fdcfc4552218b7b70", size = 51273 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/09/fe/f61e7129e9e689d9e40bbf8a36fb90f04eceb477f4617c02c6a18463e81f/configparser-7.2.0-py3-none-any.whl", hash = "sha256:fee5e1f3db4156dcd0ed95bc4edfa3580475537711f67a819c966b389d09ce62", size = 17232 },
+]
+
+[[package]]
+name = "cryptography"
+version = "45.0.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f0/fb/09e28bc0c46d2c547085e60897fea96310574c70fb21cd58a730a45f3403/cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8", size = 7043092 },
+ { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926 },
+ { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235 },
+ { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785 },
+ { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050 },
+ { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379 },
+ { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355 },
+ { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087 },
+ { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873 },
+ { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651 },
+ { url = "https://files.pythonhosted.org/packages/2e/92/cc723dd6d71e9747a887b94eb3827825c6c24b9e6ce2bb33b847d31d5eaa/cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9", size = 2929050 },
+ { url = "https://files.pythonhosted.org/packages/1f/10/197da38a5911a48dd5389c043de4aec4b3c94cb836299b01253940788d78/cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63", size = 3403224 },
+ { url = "https://files.pythonhosted.org/packages/fe/2b/160ce8c2765e7a481ce57d55eba1546148583e7b6f85514472b1d151711d/cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8", size = 7017143 },
+ { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780 },
+ { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091 },
+ { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711 },
+ { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299 },
+ { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558 },
+ { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020 },
+ { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759 },
+ { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991 },
+ { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189 },
+ { url = "https://files.pythonhosted.org/packages/f8/ea/a78a0c38f4c8736287b71c2ea3799d173d5ce778c7d6e3c163a95a05ad2a/cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f", size = 2911769 },
+ { url = "https://files.pythonhosted.org/packages/79/b3/28ac139109d9005ad3f6b6f8976ffede6706a6478e21c889ce36c840918e/cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97", size = 3390016 },
+ { url = "https://files.pythonhosted.org/packages/f8/8b/34394337abe4566848a2bd49b26bcd4b07fd466afd3e8cce4cb79a390869/cryptography-45.0.5-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:206210d03c1193f4e1ff681d22885181d47efa1ab3018766a7b32a7b3d6e6afd", size = 3575762 },
+ { url = "https://files.pythonhosted.org/packages/8b/5d/a19441c1e89afb0f173ac13178606ca6fab0d3bd3ebc29e9ed1318b507fc/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c648025b6840fe62e57107e0a25f604db740e728bd67da4f6f060f03017d5097", size = 4140906 },
+ { url = "https://files.pythonhosted.org/packages/4b/db/daceb259982a3c2da4e619f45b5bfdec0e922a23de213b2636e78ef0919b/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b8fa8b0a35a9982a3c60ec79905ba5bb090fc0b9addcfd3dc2dd04267e45f25e", size = 4374411 },
+ { url = "https://files.pythonhosted.org/packages/6a/35/5d06ad06402fc522c8bf7eab73422d05e789b4e38fe3206a85e3d6966c11/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:14d96584701a887763384f3c47f0ca7c1cce322aa1c31172680eb596b890ec30", size = 4140942 },
+ { url = "https://files.pythonhosted.org/packages/65/79/020a5413347e44c382ef1f7f7e7a66817cd6273e3e6b5a72d18177b08b2f/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57c816dfbd1659a367831baca4b775b2a5b43c003daf52e9d57e1d30bc2e1b0e", size = 4374079 },
+ { url = "https://files.pythonhosted.org/packages/9b/c5/c0e07d84a9a2a8a0ed4f865e58f37c71af3eab7d5e094ff1b21f3f3af3bc/cryptography-45.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b9e38e0a83cd51e07f5a48ff9691cae95a79bea28fe4ded168a8e5c6c77e819d", size = 3321362 },
+ { url = "https://files.pythonhosted.org/packages/c0/71/9bdbcfd58d6ff5084687fe722c58ac718ebedbc98b9f8f93781354e6d286/cryptography-45.0.5-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8c4a6ff8a30e9e3d38ac0539e9a9e02540ab3f827a3394f8852432f6b0ea152e", size = 3587878 },
+ { url = "https://files.pythonhosted.org/packages/f0/63/83516cfb87f4a8756eaa4203f93b283fda23d210fc14e1e594bd5f20edb6/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bd4c45986472694e5121084c6ebbd112aa919a25e783b87eb95953c9573906d6", size = 4152447 },
+ { url = "https://files.pythonhosted.org/packages/22/11/d2823d2a5a0bd5802b3565437add16f5c8ce1f0778bf3822f89ad2740a38/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:982518cd64c54fcada9d7e5cf28eabd3ee76bd03ab18e08a48cad7e8b6f31b18", size = 4386778 },
+ { url = "https://files.pythonhosted.org/packages/5f/38/6bf177ca6bce4fe14704ab3e93627c5b0ca05242261a2e43ef3168472540/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:12e55281d993a793b0e883066f590c1ae1e802e3acb67f8b442e721e475e6463", size = 4151627 },
+ { url = "https://files.pythonhosted.org/packages/38/6a/69fc67e5266bff68a91bcb81dff8fb0aba4d79a78521a08812048913e16f/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:5aa1e32983d4443e310f726ee4b071ab7569f58eedfdd65e9675484a4eb67bd1", size = 4385593 },
+ { url = "https://files.pythonhosted.org/packages/f6/34/31a1604c9a9ade0fdab61eb48570e09a796f4d9836121266447b0eaf7feb/cryptography-45.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e357286c1b76403dd384d938f93c46b2b058ed4dfcdce64a770f0537ed3feb6f", size = 3331106 },
+]
+
+[[package]]
+name = "distlib"
+version = "0.3.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 },
+]
+
+[[package]]
+name = "distro"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 },
+]
+
+[[package]]
+name = "dotenv"
+version = "0.9.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "python-dotenv" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892 },
+]
+
+[[package]]
+name = "ecdsa"
+version = "0.19.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c0/1f/924e3caae75f471eae4b26bd13b698f6af2c44279f67af317439c2f4c46a/ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61", size = 201793 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607 },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 },
+]
+
+[[package]]
+name = "fastapi"
+version = "0.116.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/20/38/e1da78736143fd885c36213a3ccc493c384ae8fea6a0f0bc272ef42ebea8/fastapi-0.116.0.tar.gz", hash = "sha256:80dc0794627af0390353a6d1171618276616310d37d24faba6648398e57d687a", size = 296518 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2f/68/d80347fe2360445b5f58cf290e588a4729746e7501080947e6cdae114b1f/fastapi-0.116.0-py3-none-any.whl", hash = "sha256:fdcc9ed272eaef038952923bef2b735c02372402d1203ee1210af4eea7a78d2b", size = 95625 },
+]
+
+[[package]]
+name = "filelock"
+version = "3.18.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 },
+]
+
+[[package]]
+name = "frozenlist"
+version = "1.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/36/0da0a49409f6b47cc2d060dc8c9040b897b5902a8a4e37d9bc1deb11f680/frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a", size = 81304 },
+ { url = "https://files.pythonhosted.org/packages/77/f0/77c11d13d39513b298e267b22eb6cb559c103d56f155aa9a49097221f0b6/frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61", size = 47735 },
+ { url = "https://files.pythonhosted.org/packages/37/12/9d07fa18971a44150593de56b2f2947c46604819976784bcf6ea0d5db43b/frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d", size = 46775 },
+ { url = "https://files.pythonhosted.org/packages/70/34/f73539227e06288fcd1f8a76853e755b2b48bca6747e99e283111c18bcd4/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e", size = 224644 },
+ { url = "https://files.pythonhosted.org/packages/fb/68/c1d9c2f4a6e438e14613bad0f2973567586610cc22dcb1e1241da71de9d3/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9", size = 222125 },
+ { url = "https://files.pythonhosted.org/packages/b9/d0/98e8f9a515228d708344d7c6986752be3e3192d1795f748c24bcf154ad99/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c", size = 233455 },
+ { url = "https://files.pythonhosted.org/packages/79/df/8a11bcec5600557f40338407d3e5bea80376ed1c01a6c0910fcfdc4b8993/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981", size = 227339 },
+ { url = "https://files.pythonhosted.org/packages/50/82/41cb97d9c9a5ff94438c63cc343eb7980dac4187eb625a51bdfdb7707314/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615", size = 212969 },
+ { url = "https://files.pythonhosted.org/packages/13/47/f9179ee5ee4f55629e4f28c660b3fdf2775c8bfde8f9c53f2de2d93f52a9/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50", size = 222862 },
+ { url = "https://files.pythonhosted.org/packages/1a/52/df81e41ec6b953902c8b7e3a83bee48b195cb0e5ec2eabae5d8330c78038/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa", size = 222492 },
+ { url = "https://files.pythonhosted.org/packages/84/17/30d6ea87fa95a9408245a948604b82c1a4b8b3e153cea596421a2aef2754/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577", size = 238250 },
+ { url = "https://files.pythonhosted.org/packages/8f/00/ecbeb51669e3c3df76cf2ddd66ae3e48345ec213a55e3887d216eb4fbab3/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59", size = 218720 },
+ { url = "https://files.pythonhosted.org/packages/1a/c0/c224ce0e0eb31cc57f67742071bb470ba8246623c1823a7530be0e76164c/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e", size = 232585 },
+ { url = "https://files.pythonhosted.org/packages/55/3c/34cb694abf532f31f365106deebdeac9e45c19304d83cf7d51ebbb4ca4d1/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd", size = 234248 },
+ { url = "https://files.pythonhosted.org/packages/98/c0/2052d8b6cecda2e70bd81299e3512fa332abb6dcd2969b9c80dfcdddbf75/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718", size = 221621 },
+ { url = "https://files.pythonhosted.org/packages/c5/bf/7dcebae315436903b1d98ffb791a09d674c88480c158aa171958a3ac07f0/frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e", size = 39578 },
+ { url = "https://files.pythonhosted.org/packages/8f/5f/f69818f017fa9a3d24d1ae39763e29b7f60a59e46d5f91b9c6b21622f4cd/frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464", size = 43830 },
+ { url = "https://files.pythonhosted.org/packages/34/7e/803dde33760128acd393a27eb002f2020ddb8d99d30a44bfbaab31c5f08a/frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a", size = 82251 },
+ { url = "https://files.pythonhosted.org/packages/75/a9/9c2c5760b6ba45eae11334db454c189d43d34a4c0b489feb2175e5e64277/frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750", size = 48183 },
+ { url = "https://files.pythonhosted.org/packages/47/be/4038e2d869f8a2da165f35a6befb9158c259819be22eeaf9c9a8f6a87771/frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd", size = 47107 },
+ { url = "https://files.pythonhosted.org/packages/79/26/85314b8a83187c76a37183ceed886381a5f992975786f883472fcb6dc5f2/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2", size = 237333 },
+ { url = "https://files.pythonhosted.org/packages/1f/fd/e5b64f7d2c92a41639ffb2ad44a6a82f347787abc0c7df5f49057cf11770/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f", size = 231724 },
+ { url = "https://files.pythonhosted.org/packages/20/fb/03395c0a43a5976af4bf7534759d214405fbbb4c114683f434dfdd3128ef/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30", size = 245842 },
+ { url = "https://files.pythonhosted.org/packages/d0/15/c01c8e1dffdac5d9803507d824f27aed2ba76b6ed0026fab4d9866e82f1f/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98", size = 239767 },
+ { url = "https://files.pythonhosted.org/packages/14/99/3f4c6fe882c1f5514b6848aa0a69b20cb5e5d8e8f51a339d48c0e9305ed0/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86", size = 224130 },
+ { url = "https://files.pythonhosted.org/packages/4d/83/220a374bd7b2aeba9d0725130665afe11de347d95c3620b9b82cc2fcab97/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae", size = 235301 },
+ { url = "https://files.pythonhosted.org/packages/03/3c/3e3390d75334a063181625343e8daab61b77e1b8214802cc4e8a1bb678fc/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8", size = 234606 },
+ { url = "https://files.pythonhosted.org/packages/23/1e/58232c19608b7a549d72d9903005e2d82488f12554a32de2d5fb59b9b1ba/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31", size = 248372 },
+ { url = "https://files.pythonhosted.org/packages/c0/a4/e4a567e01702a88a74ce8a324691e62a629bf47d4f8607f24bf1c7216e7f/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7", size = 229860 },
+ { url = "https://files.pythonhosted.org/packages/73/a6/63b3374f7d22268b41a9db73d68a8233afa30ed164c46107b33c4d18ecdd/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5", size = 245893 },
+ { url = "https://files.pythonhosted.org/packages/6d/eb/d18b3f6e64799a79673c4ba0b45e4cfbe49c240edfd03a68be20002eaeaa/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898", size = 246323 },
+ { url = "https://files.pythonhosted.org/packages/5a/f5/720f3812e3d06cd89a1d5db9ff6450088b8f5c449dae8ffb2971a44da506/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56", size = 233149 },
+ { url = "https://files.pythonhosted.org/packages/69/68/03efbf545e217d5db8446acfd4c447c15b7c8cf4dbd4a58403111df9322d/frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7", size = 39565 },
+ { url = "https://files.pythonhosted.org/packages/58/17/fe61124c5c333ae87f09bb67186d65038834a47d974fc10a5fadb4cc5ae1/frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d", size = 44019 },
+ { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424 },
+ { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952 },
+ { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688 },
+ { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084 },
+ { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524 },
+ { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493 },
+ { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116 },
+ { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557 },
+ { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820 },
+ { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542 },
+ { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350 },
+ { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093 },
+ { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482 },
+ { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590 },
+ { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785 },
+ { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487 },
+ { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874 },
+ { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791 },
+ { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165 },
+ { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881 },
+ { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409 },
+ { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132 },
+ { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638 },
+ { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539 },
+ { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646 },
+ { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233 },
+ { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996 },
+ { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280 },
+ { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717 },
+ { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644 },
+ { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879 },
+ { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502 },
+ { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169 },
+ { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219 },
+ { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345 },
+ { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880 },
+ { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498 },
+ { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296 },
+ { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103 },
+ { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869 },
+ { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467 },
+ { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028 },
+ { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294 },
+ { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898 },
+ { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465 },
+ { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385 },
+ { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771 },
+ { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206 },
+ { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620 },
+ { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059 },
+ { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516 },
+ { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106 },
+]
+
+[[package]]
+name = "future"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326 },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
+]
+
+[[package]]
+name = "identify"
+version = "2.6.12"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145 },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
+]
+
+[[package]]
+name = "jiter"
+version = "0.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/be/7e/4011b5c77bec97cb2b572f566220364e3e21b51c48c5bd9c4a9c26b41b67/jiter-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2fb72b02478f06a900a5782de2ef47e0396b3e1f7d5aba30daeb1fce66f303", size = 317215 },
+ { url = "https://files.pythonhosted.org/packages/8a/4f/144c1b57c39692efc7ea7d8e247acf28e47d0912800b34d0ad815f6b2824/jiter-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32bb468e3af278f095d3fa5b90314728a6916d89ba3d0ffb726dd9bf7367285e", size = 322814 },
+ { url = "https://files.pythonhosted.org/packages/63/1f/db977336d332a9406c0b1f0b82be6f71f72526a806cbb2281baf201d38e3/jiter-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8b3e0068c26ddedc7abc6fac37da2d0af16b921e288a5a613f4b86f050354f", size = 345237 },
+ { url = "https://files.pythonhosted.org/packages/d7/1c/aa30a4a775e8a672ad7f21532bdbfb269f0706b39c6ff14e1f86bdd9e5ff/jiter-0.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:286299b74cc49e25cd42eea19b72aa82c515d2f2ee12d11392c56d8701f52224", size = 370999 },
+ { url = "https://files.pythonhosted.org/packages/35/df/f8257abc4207830cb18880781b5f5b716bad5b2a22fb4330cfd357407c5b/jiter-0.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ed5649ceeaeffc28d87fb012d25a4cd356dcd53eff5acff1f0466b831dda2a7", size = 491109 },
+ { url = "https://files.pythonhosted.org/packages/06/76/9e1516fd7b4278aa13a2cc7f159e56befbea9aa65c71586305e7afa8b0b3/jiter-0.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2ab0051160cb758a70716448908ef14ad476c3774bd03ddce075f3c1f90a3d6", size = 388608 },
+ { url = "https://files.pythonhosted.org/packages/6d/64/67750672b4354ca20ca18d3d1ccf2c62a072e8a2d452ac3cf8ced73571ef/jiter-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03997d2f37f6b67d2f5c475da4412be584e1cec273c1cfc03d642c46db43f8cf", size = 352454 },
+ { url = "https://files.pythonhosted.org/packages/96/4d/5c4e36d48f169a54b53a305114be3efa2bbffd33b648cd1478a688f639c1/jiter-0.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c404a99352d839fed80d6afd6c1d66071f3bacaaa5c4268983fc10f769112e90", size = 391833 },
+ { url = "https://files.pythonhosted.org/packages/0b/de/ce4a6166a78810bd83763d2fa13f85f73cbd3743a325469a4a9289af6dae/jiter-0.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66e989410b6666d3ddb27a74c7e50d0829704ede652fd4c858e91f8d64b403d0", size = 523646 },
+ { url = "https://files.pythonhosted.org/packages/a2/a6/3bc9acce53466972964cf4ad85efecb94f9244539ab6da1107f7aed82934/jiter-0.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b532d3af9ef4f6374609a3bcb5e05a1951d3bf6190dc6b176fdb277c9bbf15ee", size = 514735 },
+ { url = "https://files.pythonhosted.org/packages/b4/d8/243c2ab8426a2a4dea85ba2a2ba43df379ccece2145320dfd4799b9633c5/jiter-0.10.0-cp310-cp310-win32.whl", hash = "sha256:da9be20b333970e28b72edc4dff63d4fec3398e05770fb3205f7fb460eb48dd4", size = 210747 },
+ { url = "https://files.pythonhosted.org/packages/37/7a/8021bd615ef7788b98fc76ff533eaac846322c170e93cbffa01979197a45/jiter-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:f59e533afed0c5b0ac3eba20d2548c4a550336d8282ee69eb07b37ea526ee4e5", size = 207484 },
+ { url = "https://files.pythonhosted.org/packages/1b/dd/6cefc6bd68b1c3c979cecfa7029ab582b57690a31cd2f346c4d0ce7951b6/jiter-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3bebe0c558e19902c96e99217e0b8e8b17d570906e72ed8a87170bc290b1e978", size = 317473 },
+ { url = "https://files.pythonhosted.org/packages/be/cf/fc33f5159ce132be1d8dd57251a1ec7a631c7df4bd11e1cd198308c6ae32/jiter-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc", size = 321971 },
+ { url = "https://files.pythonhosted.org/packages/68/a4/da3f150cf1d51f6c472616fb7650429c7ce053e0c962b41b68557fdf6379/jiter-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d", size = 345574 },
+ { url = "https://files.pythonhosted.org/packages/84/34/6e8d412e60ff06b186040e77da5f83bc158e9735759fcae65b37d681f28b/jiter-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2", size = 371028 },
+ { url = "https://files.pythonhosted.org/packages/fb/d9/9ee86173aae4576c35a2f50ae930d2ccb4c4c236f6cb9353267aa1d626b7/jiter-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61", size = 491083 },
+ { url = "https://files.pythonhosted.org/packages/d9/2c/f955de55e74771493ac9e188b0f731524c6a995dffdcb8c255b89c6fb74b/jiter-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db", size = 388821 },
+ { url = "https://files.pythonhosted.org/packages/81/5a/0e73541b6edd3f4aada586c24e50626c7815c561a7ba337d6a7eb0a915b4/jiter-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5", size = 352174 },
+ { url = "https://files.pythonhosted.org/packages/1c/c0/61eeec33b8c75b31cae42be14d44f9e6fe3ac15a4e58010256ac3abf3638/jiter-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc347c87944983481e138dea467c0551080c86b9d21de6ea9306efb12ca8f606", size = 391869 },
+ { url = "https://files.pythonhosted.org/packages/41/22/5beb5ee4ad4ef7d86f5ea5b4509f680a20706c4a7659e74344777efb7739/jiter-0.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605", size = 523741 },
+ { url = "https://files.pythonhosted.org/packages/ea/10/768e8818538e5817c637b0df52e54366ec4cebc3346108a4457ea7a98f32/jiter-0.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5", size = 514527 },
+ { url = "https://files.pythonhosted.org/packages/73/6d/29b7c2dc76ce93cbedabfd842fc9096d01a0550c52692dfc33d3cc889815/jiter-0.10.0-cp311-cp311-win32.whl", hash = "sha256:db16e4848b7e826edca4ccdd5b145939758dadf0dc06e7007ad0e9cfb5928ae7", size = 210765 },
+ { url = "https://files.pythonhosted.org/packages/c2/c9/d394706deb4c660137caf13e33d05a031d734eb99c051142e039d8ceb794/jiter-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c9c1d5f10e18909e993f9641f12fe1c77b3e9b533ee94ffa970acc14ded3812", size = 209234 },
+ { url = "https://files.pythonhosted.org/packages/6d/b5/348b3313c58f5fbfb2194eb4d07e46a35748ba6e5b3b3046143f3040bafa/jiter-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e274728e4a5345a6dde2d343c8da018b9d4bd4350f5a472fa91f66fda44911b", size = 312262 },
+ { url = "https://files.pythonhosted.org/packages/9c/4a/6a2397096162b21645162825f058d1709a02965606e537e3304b02742e9b/jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744", size = 320124 },
+ { url = "https://files.pythonhosted.org/packages/2a/85/1ce02cade7516b726dd88f59a4ee46914bf79d1676d1228ef2002ed2f1c9/jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2", size = 345330 },
+ { url = "https://files.pythonhosted.org/packages/75/d0/bb6b4f209a77190ce10ea8d7e50bf3725fc16d3372d0a9f11985a2b23eff/jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026", size = 369670 },
+ { url = "https://files.pythonhosted.org/packages/a0/f5/a61787da9b8847a601e6827fbc42ecb12be2c925ced3252c8ffcb56afcaf/jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c", size = 489057 },
+ { url = "https://files.pythonhosted.org/packages/12/e4/6f906272810a7b21406c760a53aadbe52e99ee070fc5c0cb191e316de30b/jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959", size = 389372 },
+ { url = "https://files.pythonhosted.org/packages/e2/ba/77013b0b8ba904bf3762f11e0129b8928bff7f978a81838dfcc958ad5728/jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a", size = 352038 },
+ { url = "https://files.pythonhosted.org/packages/67/27/c62568e3ccb03368dbcc44a1ef3a423cb86778a4389e995125d3d1aaa0a4/jiter-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6842184aed5cdb07e0c7e20e5bdcfafe33515ee1741a6835353bb45fe5d1bd95", size = 391538 },
+ { url = "https://files.pythonhosted.org/packages/c0/72/0d6b7e31fc17a8fdce76164884edef0698ba556b8eb0af9546ae1a06b91d/jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea", size = 523557 },
+ { url = "https://files.pythonhosted.org/packages/2f/09/bc1661fbbcbeb6244bd2904ff3a06f340aa77a2b94e5a7373fd165960ea3/jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b", size = 514202 },
+ { url = "https://files.pythonhosted.org/packages/1b/84/5a5d5400e9d4d54b8004c9673bbe4403928a00d28529ff35b19e9d176b19/jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01", size = 211781 },
+ { url = "https://files.pythonhosted.org/packages/9b/52/7ec47455e26f2d6e5f2ea4951a0652c06e5b995c291f723973ae9e724a65/jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49", size = 206176 },
+ { url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617 },
+ { url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947 },
+ { url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618 },
+ { url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829 },
+ { url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034 },
+ { url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529 },
+ { url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671 },
+ { url = "https://files.pythonhosted.org/packages/c6/77/71b0b24cbcc28f55ab4dbfe029f9a5b73aeadaba677843fc6dc9ed2b1d0a/jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca", size = 390864 },
+ { url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989 },
+ { url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495 },
+ { url = "https://files.pythonhosted.org/packages/9c/36/3468e5a18238bdedae7c4d19461265b5e9b8e288d3f86cd89d00cbb48686/jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d", size = 211289 },
+ { url = "https://files.pythonhosted.org/packages/7e/07/1c96b623128bcb913706e294adb5f768fb7baf8db5e1338ce7b4ee8c78ef/jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4", size = 205074 },
+ { url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225 },
+ { url = "https://files.pythonhosted.org/packages/43/84/c7d44c75767e18946219ba2d703a5a32ab37b0bc21886a97bc6062e4da42/jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070", size = 350235 },
+ { url = "https://files.pythonhosted.org/packages/01/16/f5a0135ccd968b480daad0e6ab34b0c7c5ba3bc447e5088152696140dcb3/jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca", size = 207278 },
+ { url = "https://files.pythonhosted.org/packages/1c/9b/1d646da42c3de6c2188fdaa15bce8ecb22b635904fc68be025e21249ba44/jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522", size = 310866 },
+ { url = "https://files.pythonhosted.org/packages/ad/0e/26538b158e8a7c7987e94e7aeb2999e2e82b1f9d2e1f6e9874ddf71ebda0/jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8", size = 318772 },
+ { url = "https://files.pythonhosted.org/packages/7b/fb/d302893151caa1c2636d6574d213e4b34e31fd077af6050a9c5cbb42f6fb/jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216", size = 344534 },
+ { url = "https://files.pythonhosted.org/packages/01/d8/5780b64a149d74e347c5128d82176eb1e3241b1391ac07935693466d6219/jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4", size = 369087 },
+ { url = "https://files.pythonhosted.org/packages/e8/5b/f235a1437445160e777544f3ade57544daf96ba7e96c1a5b24a6f7ac7004/jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426", size = 490694 },
+ { url = "https://files.pythonhosted.org/packages/85/a9/9c3d4617caa2ff89cf61b41e83820c27ebb3f7b5fae8a72901e8cd6ff9be/jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12", size = 388992 },
+ { url = "https://files.pythonhosted.org/packages/68/b1/344fd14049ba5c94526540af7eb661871f9c54d5f5601ff41a959b9a0bbd/jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9", size = 351723 },
+ { url = "https://files.pythonhosted.org/packages/41/89/4c0e345041186f82a31aee7b9d4219a910df672b9fef26f129f0cda07a29/jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a", size = 392215 },
+ { url = "https://files.pythonhosted.org/packages/55/58/ee607863e18d3f895feb802154a2177d7e823a7103f000df182e0f718b38/jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853", size = 522762 },
+ { url = "https://files.pythonhosted.org/packages/15/d0/9123fb41825490d16929e73c212de9a42913d68324a8ce3c8476cae7ac9d/jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86", size = 513427 },
+ { url = "https://files.pythonhosted.org/packages/d8/b3/2bd02071c5a2430d0b70403a34411fc519c2f227da7b03da9ba6a956f931/jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357", size = 210127 },
+ { url = "https://files.pythonhosted.org/packages/03/0c/5fe86614ea050c3ecd728ab4035534387cd41e7c1855ef6c031f1ca93e3f/jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00", size = 318527 },
+ { url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213 },
+]
+
+[[package]]
+name = "json-repair"
+version = "0.51.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4f/3a/f30f3c92da3a285dcbe469c50b058f2d349dc9a20fc1b60c3219befda53f/json_repair-0.51.0.tar.gz", hash = "sha256:487e00042d5bc5cc4897ea9c3cccd4f6641e926b732cc09f98691a832485098a", size = 35289 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/fc/eb15e39547b29dbf2b786bbbd1e79e7f1d87ec4e7c9ea61786f093181481/json_repair-0.51.0-py3-none-any.whl", hash = "sha256:871f7651ee82abf72efc50a80d3a9af0ade8abf5b4541b418eeeabe4e677e314", size = 26263 },
+]
+
+[[package]]
+name = "kuzu"
+version = "0.10.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b6/07/57b34a7ef11e421ed8a610d6c52b86501077666ddaa506d0f382da17727f/kuzu-0.10.1.tar.gz", hash = "sha256:f45aa05e1a19e6ab4dc02c1239d70a180f8ba73971a2babacd15668e0673e4a4", size = 4856334 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/8b/b1b66be1342e5a0b86f8055a37cd686cb3a73e567f8f922d590a89754790/kuzu-0.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fdfd72bee80035139b494c4fd5dca5de8fafba9954706be38201760fe40b419c", size = 3687133 },
+ { url = "https://files.pythonhosted.org/packages/b1/1a/165b8e56570a14b7ed51bb194e9a8d806ff0a9bbf89f76ba94297bb03bdf/kuzu-0.10.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5d4b9f7f19d750e993a151655d799c675a9f6d6439212d3b973c2dcdcec1837e", size = 4147639 },
+ { url = "https://files.pythonhosted.org/packages/b0/d8/e6e004e06b1da1c5b902164b12129327e3214a9079462f1f7fdf81a1544e/kuzu-0.10.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa177b9bd0269883e1abfdea03872ce835370f560da1e013160e7c75cb33451e", size = 6097094 },
+ { url = "https://files.pythonhosted.org/packages/49/22/37f3a33e676c6066515f1d95e0de90c20a0c6492823e98d9caca4bc9c755/kuzu-0.10.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8a83dc10d8f0c8771bdaae7d2cc55c431f909e7ef164d93d55e9d80936c7254", size = 6893871 },
+ { url = "https://files.pythonhosted.org/packages/6b/58/5663cfd028acb0f7952f563d1fb5427fbf5ef698fa8f3ae57918b5dc1365/kuzu-0.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:0c0a2fb97932c0b5d3794d181145e7c52ab5f6ac9235de36f4d87396612e2a57", size = 4211834 },
+ { url = "https://files.pythonhosted.org/packages/72/bc/250ad66006fa3acd0bcd8cf2f483a3ff3644efb701270a9da37df3538cc8/kuzu-0.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aa4ef41f7e7da5b8f28f998dc9acfe26d861e61be5f0b631ad8edad8f39f360f", size = 3687472 },
+ { url = "https://files.pythonhosted.org/packages/a9/01/21f1cb58aedb91aa882fd796a3ff99fa9f946705faf06b35d5184787467a/kuzu-0.10.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:1b5134a1790a7eb1ece7c46e8fa5d4893f9eae4aeecc61351405bcbcd00920da", size = 4149732 },
+ { url = "https://files.pythonhosted.org/packages/01/a8/bae01ff55be91448fe6395ab5b20d377437649d3fc5f1c04c2395123b157/kuzu-0.10.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82e237e31c636361df493d1bb1401b171a5b58b5add84f1221b6071bc5c4cc0b", size = 6097809 },
+ { url = "https://files.pythonhosted.org/packages/6b/0d/d545ed5759514374146010a965bf562b85a2b0132d6b33a648cbbdc88338/kuzu-0.10.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0958bda27d4df4be4963f6c3822626aada5cb5123c58c89d89ede54b886eed7e", size = 6893780 },
+ { url = "https://files.pythonhosted.org/packages/9f/7a/03c6f36352e8396ee3353fc8f9772bb6c51a84051f80074a5d90c18188d4/kuzu-0.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:90172c9253bbc1ea25b566c3fa21597e286df2a062394967c9f5eea258832ab1", size = 4212749 },
+ { url = "https://files.pythonhosted.org/packages/5a/48/68069c0a4b4a6f38795d0831d3d7a4c1faeb7f3e7cd6f81e68d077029ecc/kuzu-0.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffdc24218b94d444f872deb8625a72df70e141c4080132f5d23eef4ad5904b27", size = 3687775 },
+ { url = "https://files.pythonhosted.org/packages/a1/eb/05564aa8a7607487a75b7960dcf5e2c4decb577593e03c34aad8a78ab93a/kuzu-0.10.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:f572f322d2ffe145511c2bcf6df7ba2170a08368341cbea1812610cafcb04931", size = 4151602 },
+ { url = "https://files.pythonhosted.org/packages/e9/cf/4875fe776c950b1428d663ccf1e2e027628e171f8b5416bee2facd09a734/kuzu-0.10.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a864cc7fea50585cc4285b118c7cc6591835cc03f72b4eb3fb6dfc46502f5ba3", size = 6097739 },
+ { url = "https://files.pythonhosted.org/packages/bf/ba/ce9302508a4ee1a53f1a6666a22abf653eb1b63d352e4b0ff350e1d0e0f1/kuzu-0.10.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:917366d4e5c65fdae7449e4546aeb520f7d50ec2dd8ec3191b0f8be9003d6061", size = 6893053 },
+ { url = "https://files.pythonhosted.org/packages/3a/d1/aad1c8751745ebd14776b59ca8bd853664a1d2d092697f2c05cdd9fd5602/kuzu-0.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:b9607cdc0c2fb2df0a49b7708eb6bd6032dc4ba893c5335990b9256ca31a3e82", size = 4214043 },
+ { url = "https://files.pythonhosted.org/packages/c9/49/068889d812965512badeb8e2e0415b6b3fc8593b056c343ee01465b1faf7/kuzu-0.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1fd4993683bc3c7d4db37450b72fa5f070251741bcb41dafeb866d39e66c50bc", size = 3687840 },
+ { url = "https://files.pythonhosted.org/packages/15/3a/dc84dcdff5a80f524c809fbe9de07fd0f5e4fb9f5f530a3f7aa3485c586d/kuzu-0.10.1-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:bed8efd36b7b86f43949b5d1aca42e3ef7a77269ee1bee424ef5795d6fa56e41", size = 4151490 },
+ { url = "https://files.pythonhosted.org/packages/9a/f2/f5a6a2d23c2fc3d2cd8d0ec33639d6dfa66852d66becbc0b046f8df2a9eb/kuzu-0.10.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99d66c554678b7819cf32847a7429ef93e8dda7c7af2c3f84d550126523f429f", size = 6097518 },
+ { url = "https://files.pythonhosted.org/packages/26/6e/6de20c949e0dedc341166622218f5129bf98c5e4e9b73795bfd557f84fd0/kuzu-0.10.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:107ea216cae952f7efa6a919aac24fcc9add2ae4e20011f4b50970753b3b7f61", size = 6892771 },
+ { url = "https://files.pythonhosted.org/packages/4e/41/c1aa1246899c028c98d6db7db94150787bc9a7537132d9bc64146b3a1bef/kuzu-0.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:b5ed08c8acaf41b5f1d40e22937cb2d3e0832b6ef1cf1014ece21a0ebe9c03eb", size = 4214094 },
+ { url = "https://files.pythonhosted.org/packages/19/b7/f4bef6516801dac0e8e30b45b50de0a048ac85cc79cdb4cd8909293d16b8/kuzu-0.10.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1d60792b29a53d3c8defee31930f74c9b9e2fe4406cb33f33f6f08cbbf65dcf", size = 6101052 },
+ { url = "https://files.pythonhosted.org/packages/81/91/47407addc35cf0802399c94522df5c1685d2e1e9994a5fa193c13a004460/kuzu-0.10.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec80822a36b34860a0955d36037d2047db853544d9eb43c4a6d7026900c007a8", size = 6897341 },
+]
+
+[[package]]
+name = "lightrag-hku"
+source = { editable = "." }
+dependencies = [
+ { name = "aiohttp" },
+ { name = "configparser" },
+ { name = "dotenv" },
+ { name = "future" },
+ { name = "httpx" },
+ { name = "json-repair" },
+ { name = "kuzu" },
+ { name = "nano-vectordb" },
+ { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "pandas" },
+ { name = "pipmaster" },
+ { name = "pydantic" },
+ { name = "pypinyin" },
+ { name = "python-dotenv" },
+ { name = "setuptools" },
+ { name = "tenacity" },
+ { name = "tiktoken" },
+ { name = "xlsxwriter" },
+]
+
+[package.optional-dependencies]
+api = [
+ { name = "aiofiles" },
+ { name = "aiohttp" },
+ { name = "ascii-colors" },
+ { name = "asyncpg" },
+ { name = "configparser" },
+ { name = "distro" },
+ { name = "dotenv" },
+ { name = "fastapi" },
+ { name = "future" },
+ { name = "httpcore" },
+ { name = "httpx" },
+ { name = "jiter" },
+ { name = "nano-vectordb" },
+ { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "openai" },
+ { name = "pandas" },
+ { name = "passlib", extra = ["bcrypt"] },
+ { name = "pipmaster" },
+ { name = "psutil" },
+ { name = "pydantic" },
+ { name = "pyjwt" },
+ { name = "pypinyin" },
+ { name = "python-dotenv" },
+ { name = "python-jose", extra = ["cryptography"] },
+ { name = "python-multipart" },
+ { name = "pytz" },
+ { name = "setuptools" },
+ { name = "tenacity" },
+ { name = "tiktoken" },
+ { name = "uvicorn" },
+ { name = "xlsxwriter" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "pre-commit" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "aiofiles", marker = "extra == 'api'" },
+ { name = "aiohttp" },
+ { name = "aiohttp", marker = "extra == 'api'" },
+ { name = "ascii-colors", marker = "extra == 'api'" },
+ { name = "asyncpg", marker = "extra == 'api'" },
+ { name = "configparser" },
+ { name = "configparser", marker = "extra == 'api'" },
+ { name = "distro", marker = "extra == 'api'" },
+ { name = "dotenv" },
+ { name = "dotenv", marker = "extra == 'api'" },
+ { name = "fastapi", marker = "extra == 'api'" },
+ { name = "future" },
+ { name = "future", marker = "extra == 'api'" },
+ { name = "httpcore", marker = "extra == 'api'" },
+ { name = "httpx", specifier = ">=0.28.1" },
+ { name = "httpx", marker = "extra == 'api'" },
+ { name = "jiter", marker = "extra == 'api'" },
+ { name = "json-repair" },
+ { name = "kuzu", specifier = ">=0.10.1" },
+ { name = "nano-vectordb" },
+ { name = "nano-vectordb", marker = "extra == 'api'" },
+ { name = "networkx" },
+ { name = "networkx", specifier = ">=3.4.2" },
+ { name = "networkx", marker = "extra == 'api'" },
+ { name = "numpy" },
+ { name = "numpy", marker = "extra == 'api'" },
+ { name = "openai", marker = "extra == 'api'" },
+ { name = "pandas", specifier = ">=2.0.0" },
+ { name = "pandas", marker = "extra == 'api'", specifier = ">=2.0.0" },
+ { name = "passlib", extras = ["bcrypt"], marker = "extra == 'api'" },
+ { name = "pipmaster" },
+ { name = "pipmaster", marker = "extra == 'api'" },
+ { name = "psutil", marker = "extra == 'api'" },
+ { name = "pydantic" },
+ { name = "pydantic", marker = "extra == 'api'" },
+ { name = "pyjwt", marker = "extra == 'api'" },
+ { name = "pypinyin" },
+ { name = "pypinyin", marker = "extra == 'api'" },
+ { name = "python-dotenv" },
+ { name = "python-dotenv", marker = "extra == 'api'" },
+ { name = "python-jose", extras = ["cryptography"], marker = "extra == 'api'" },
+ { name = "python-multipart", marker = "extra == 'api'" },
+ { name = "pytz", marker = "extra == 'api'" },
+ { name = "setuptools" },
+ { name = "setuptools", marker = "extra == 'api'" },
+ { name = "tenacity" },
+ { name = "tenacity", marker = "extra == 'api'" },
+ { name = "tiktoken" },
+ { name = "tiktoken", marker = "extra == 'api'" },
+ { name = "uvicorn", marker = "extra == 'api'" },
+ { name = "xlsxwriter", specifier = ">=3.1.0" },
+ { name = "xlsxwriter", marker = "extra == 'api'", specifier = ">=3.1.0" },
+]
+provides-extras = ["api"]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "pre-commit", specifier = ">=4.2.0" },
+ { name = "pytest", specifier = ">=8.4.1" },
+ { name = "pytest-asyncio", specifier = ">=1.0.0" },
+]
+
+[[package]]
+name = "multidict"
+version = "6.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/5dad12e82fbdf7470f29bff2171484bf07cb3b16ada60a6589af8f376440/multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc", size = 101006 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/67/414933982bce2efce7cbcb3169eaaf901e0f25baec69432b4874dfb1f297/multidict-6.6.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a2be5b7b35271f7fff1397204ba6708365e3d773579fe2a30625e16c4b4ce817", size = 77017 },
+ { url = "https://files.pythonhosted.org/packages/8a/fe/d8a3ee1fad37dc2ef4f75488b0d9d4f25bf204aad8306cbab63d97bff64a/multidict-6.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12f4581d2930840295c461764b9a65732ec01250b46c6b2c510d7ee68872b140", size = 44897 },
+ { url = "https://files.pythonhosted.org/packages/1f/e0/265d89af8c98240265d82b8cbcf35897f83b76cd59ee3ab3879050fd8c45/multidict-6.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dd7793bab517e706c9ed9d7310b06c8672fd0aeee5781bfad612f56b8e0f7d14", size = 44574 },
+ { url = "https://files.pythonhosted.org/packages/e6/05/6b759379f7e8e04ccc97cfb2a5dcc5cdbd44a97f072b2272dc51281e6a40/multidict-6.6.3-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:72d8815f2cd3cf3df0f83cac3f3ef801d908b2d90409ae28102e0553af85545a", size = 225729 },
+ { url = "https://files.pythonhosted.org/packages/4e/f5/8d5a15488edd9a91fa4aad97228d785df208ed6298580883aa3d9def1959/multidict-6.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:531e331a2ee53543ab32b16334e2deb26f4e6b9b28e41f8e0c87e99a6c8e2d69", size = 242515 },
+ { url = "https://files.pythonhosted.org/packages/6e/b5/a8f317d47d0ac5bb746d6d8325885c8967c2a8ce0bb57be5399e3642cccb/multidict-6.6.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:42ca5aa9329a63be8dc49040f63817d1ac980e02eeddba763a9ae5b4027b9c9c", size = 222224 },
+ { url = "https://files.pythonhosted.org/packages/76/88/18b2a0d5e80515fa22716556061189c2853ecf2aa2133081ebbe85ebea38/multidict-6.6.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:208b9b9757060b9faa6f11ab4bc52846e4f3c2fb8b14d5680c8aac80af3dc751", size = 253124 },
+ { url = "https://files.pythonhosted.org/packages/62/bf/ebfcfd6b55a1b05ef16d0775ae34c0fe15e8dab570d69ca9941073b969e7/multidict-6.6.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:acf6b97bd0884891af6a8b43d0f586ab2fcf8e717cbd47ab4bdddc09e20652d8", size = 251529 },
+ { url = "https://files.pythonhosted.org/packages/44/11/780615a98fd3775fc309d0234d563941af69ade2df0bb82c91dda6ddaea1/multidict-6.6.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68e9e12ed00e2089725669bdc88602b0b6f8d23c0c95e52b95f0bc69f7fe9b55", size = 241627 },
+ { url = "https://files.pythonhosted.org/packages/28/3d/35f33045e21034b388686213752cabc3a1b9d03e20969e6fa8f1b1d82db1/multidict-6.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:05db2f66c9addb10cfa226e1acb363450fab2ff8a6df73c622fefe2f5af6d4e7", size = 239351 },
+ { url = "https://files.pythonhosted.org/packages/6e/cc/ff84c03b95b430015d2166d9aae775a3985d757b94f6635010d0038d9241/multidict-6.6.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0db58da8eafb514db832a1b44f8fa7906fdd102f7d982025f816a93ba45e3dcb", size = 233429 },
+ { url = "https://files.pythonhosted.org/packages/2e/f0/8cd49a0b37bdea673a4b793c2093f2f4ba8e7c9d6d7c9bd672fd6d38cd11/multidict-6.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:14117a41c8fdb3ee19c743b1c027da0736fdb79584d61a766da53d399b71176c", size = 243094 },
+ { url = "https://files.pythonhosted.org/packages/96/19/5d9a0cfdafe65d82b616a45ae950975820289069f885328e8185e64283c2/multidict-6.6.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:877443eaaabcd0b74ff32ebeed6f6176c71850feb7d6a1d2db65945256ea535c", size = 248957 },
+ { url = "https://files.pythonhosted.org/packages/e6/dc/c90066151da87d1e489f147b9b4327927241e65f1876702fafec6729c014/multidict-6.6.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:70b72e749a4f6e7ed8fb334fa8d8496384840319512746a5f42fa0aec79f4d61", size = 243590 },
+ { url = "https://files.pythonhosted.org/packages/ec/39/458afb0cccbb0ee9164365273be3e039efddcfcb94ef35924b7dbdb05db0/multidict-6.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43571f785b86afd02b3855c5ac8e86ec921b760298d6f82ff2a61daf5a35330b", size = 237487 },
+ { url = "https://files.pythonhosted.org/packages/35/38/0016adac3990426610a081787011177e661875546b434f50a26319dc8372/multidict-6.6.3-cp310-cp310-win32.whl", hash = "sha256:20c5a0c3c13a15fd5ea86c42311859f970070e4e24de5a550e99d7c271d76318", size = 41390 },
+ { url = "https://files.pythonhosted.org/packages/f3/d2/17897a8f3f2c5363d969b4c635aa40375fe1f09168dc09a7826780bfb2a4/multidict-6.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:ab0a34a007704c625e25a9116c6770b4d3617a071c8a7c30cd338dfbadfe6485", size = 45954 },
+ { url = "https://files.pythonhosted.org/packages/2d/5f/d4a717c1e457fe44072e33fa400d2b93eb0f2819c4d669381f925b7cba1f/multidict-6.6.3-cp310-cp310-win_arm64.whl", hash = "sha256:769841d70ca8bdd140a715746199fc6473414bd02efd678d75681d2d6a8986c5", size = 42981 },
+ { url = "https://files.pythonhosted.org/packages/08/f0/1a39863ced51f639c81a5463fbfa9eb4df59c20d1a8769ab9ef4ca57ae04/multidict-6.6.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:18f4eba0cbac3546b8ae31e0bbc55b02c801ae3cbaf80c247fcdd89b456ff58c", size = 76445 },
+ { url = "https://files.pythonhosted.org/packages/c9/0e/a7cfa451c7b0365cd844e90b41e21fab32edaa1e42fc0c9f68461ce44ed7/multidict-6.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef43b5dd842382329e4797c46f10748d8c2b6e0614f46b4afe4aee9ac33159df", size = 44610 },
+ { url = "https://files.pythonhosted.org/packages/c6/bb/a14a4efc5ee748cc1904b0748be278c31b9295ce5f4d2ef66526f410b94d/multidict-6.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bd1fd5eec01494e0f2e8e446a74a85d5e49afb63d75a9934e4a5423dba21d", size = 44267 },
+ { url = "https://files.pythonhosted.org/packages/c2/f8/410677d563c2d55e063ef74fe578f9d53fe6b0a51649597a5861f83ffa15/multidict-6.6.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5bd8d6f793a787153956cd35e24f60485bf0651c238e207b9a54f7458b16d539", size = 230004 },
+ { url = "https://files.pythonhosted.org/packages/fd/df/2b787f80059314a98e1ec6a4cc7576244986df3e56b3c755e6fc7c99e038/multidict-6.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bf99b4daf908c73856bd87ee0a2499c3c9a3d19bb04b9c6025e66af3fd07462", size = 247196 },
+ { url = "https://files.pythonhosted.org/packages/05/f2/f9117089151b9a8ab39f9019620d10d9718eec2ac89e7ca9d30f3ec78e96/multidict-6.6.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b9e59946b49dafaf990fd9c17ceafa62976e8471a14952163d10a7a630413a9", size = 225337 },
+ { url = "https://files.pythonhosted.org/packages/93/2d/7115300ec5b699faa152c56799b089a53ed69e399c3c2d528251f0aeda1a/multidict-6.6.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e2db616467070d0533832d204c54eea6836a5e628f2cb1e6dfd8cd6ba7277cb7", size = 257079 },
+ { url = "https://files.pythonhosted.org/packages/15/ea/ff4bab367623e39c20d3b07637225c7688d79e4f3cc1f3b9f89867677f9a/multidict-6.6.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7394888236621f61dcdd25189b2768ae5cc280f041029a5bcf1122ac63df79f9", size = 255461 },
+ { url = "https://files.pythonhosted.org/packages/74/07/2c9246cda322dfe08be85f1b8739646f2c4c5113a1422d7a407763422ec4/multidict-6.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f114d8478733ca7388e7c7e0ab34b72547476b97009d643644ac33d4d3fe1821", size = 246611 },
+ { url = "https://files.pythonhosted.org/packages/a8/62/279c13d584207d5697a752a66ffc9bb19355a95f7659140cb1b3cf82180e/multidict-6.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cdf22e4db76d323bcdc733514bf732e9fb349707c98d341d40ebcc6e9318ef3d", size = 243102 },
+ { url = "https://files.pythonhosted.org/packages/69/cc/e06636f48c6d51e724a8bc8d9e1db5f136fe1df066d7cafe37ef4000f86a/multidict-6.6.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e995a34c3d44ab511bfc11aa26869b9d66c2d8c799fa0e74b28a473a692532d6", size = 238693 },
+ { url = "https://files.pythonhosted.org/packages/89/a4/66c9d8fb9acf3b226cdd468ed009537ac65b520aebdc1703dd6908b19d33/multidict-6.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:766a4a5996f54361d8d5a9050140aa5362fe48ce51c755a50c0bc3706460c430", size = 246582 },
+ { url = "https://files.pythonhosted.org/packages/cf/01/c69e0317be556e46257826d5449feb4e6aa0d18573e567a48a2c14156f1f/multidict-6.6.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3893a0d7d28a7fe6ca7a1f760593bc13038d1d35daf52199d431b61d2660602b", size = 253355 },
+ { url = "https://files.pythonhosted.org/packages/c0/da/9cc1da0299762d20e626fe0042e71b5694f9f72d7d3f9678397cbaa71b2b/multidict-6.6.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:934796c81ea996e61914ba58064920d6cad5d99140ac3167901eb932150e2e56", size = 247774 },
+ { url = "https://files.pythonhosted.org/packages/e6/91/b22756afec99cc31105ddd4a52f95ab32b1a4a58f4d417979c570c4a922e/multidict-6.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9ed948328aec2072bc00f05d961ceadfd3e9bfc2966c1319aeaf7b7c21219183", size = 242275 },
+ { url = "https://files.pythonhosted.org/packages/be/f1/adcc185b878036a20399d5be5228f3cbe7f823d78985d101d425af35c800/multidict-6.6.3-cp311-cp311-win32.whl", hash = "sha256:9f5b28c074c76afc3e4c610c488e3493976fe0e596dd3db6c8ddfbb0134dcac5", size = 41290 },
+ { url = "https://files.pythonhosted.org/packages/e0/d4/27652c1c6526ea6b4f5ddd397e93f4232ff5de42bea71d339bc6a6cc497f/multidict-6.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc7f6fbc61b1c16050a389c630da0b32fc6d4a3d191394ab78972bf5edc568c2", size = 45942 },
+ { url = "https://files.pythonhosted.org/packages/16/18/23f4932019804e56d3c2413e237f866444b774b0263bcb81df2fdecaf593/multidict-6.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:d4e47d8faffaae822fb5cba20937c048d4f734f43572e7079298a6c39fb172cb", size = 42880 },
+ { url = "https://files.pythonhosted.org/packages/0e/a0/6b57988ea102da0623ea814160ed78d45a2645e4bbb499c2896d12833a70/multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6", size = 76514 },
+ { url = "https://files.pythonhosted.org/packages/07/7a/d1e92665b0850c6c0508f101f9cf0410c1afa24973e1115fe9c6a185ebf7/multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f", size = 45394 },
+ { url = "https://files.pythonhosted.org/packages/52/6f/dd104490e01be6ef8bf9573705d8572f8c2d2c561f06e3826b081d9e6591/multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55", size = 43590 },
+ { url = "https://files.pythonhosted.org/packages/44/fe/06e0e01b1b0611e6581b7fd5a85b43dacc08b6cea3034f902f383b0873e5/multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b", size = 237292 },
+ { url = "https://files.pythonhosted.org/packages/ce/71/4f0e558fb77696b89c233c1ee2d92f3e1d5459070a0e89153c9e9e804186/multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888", size = 258385 },
+ { url = "https://files.pythonhosted.org/packages/e3/25/cca0e68228addad24903801ed1ab42e21307a1b4b6dd2cf63da5d3ae082a/multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d", size = 242328 },
+ { url = "https://files.pythonhosted.org/packages/6e/a3/46f2d420d86bbcb8fe660b26a10a219871a0fbf4d43cb846a4031533f3e0/multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680", size = 268057 },
+ { url = "https://files.pythonhosted.org/packages/9e/73/1c743542fe00794a2ec7466abd3f312ccb8fad8dff9f36d42e18fb1ec33e/multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a", size = 269341 },
+ { url = "https://files.pythonhosted.org/packages/a4/11/6ec9dcbe2264b92778eeb85407d1df18812248bf3506a5a1754bc035db0c/multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961", size = 256081 },
+ { url = "https://files.pythonhosted.org/packages/9b/2b/631b1e2afeb5f1696846d747d36cda075bfdc0bc7245d6ba5c319278d6c4/multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65", size = 253581 },
+ { url = "https://files.pythonhosted.org/packages/bf/0e/7e3b93f79efeb6111d3bf9a1a69e555ba1d07ad1c11bceb56b7310d0d7ee/multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643", size = 250750 },
+ { url = "https://files.pythonhosted.org/packages/ad/9e/086846c1d6601948e7de556ee464a2d4c85e33883e749f46b9547d7b0704/multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063", size = 251548 },
+ { url = "https://files.pythonhosted.org/packages/8c/7b/86ec260118e522f1a31550e87b23542294880c97cfbf6fb18cc67b044c66/multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3", size = 262718 },
+ { url = "https://files.pythonhosted.org/packages/8c/bd/22ce8f47abb0be04692c9fc4638508b8340987b18691aa7775d927b73f72/multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75", size = 259603 },
+ { url = "https://files.pythonhosted.org/packages/07/9c/91b7ac1691be95cd1f4a26e36a74b97cda6aa9820632d31aab4410f46ebd/multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10", size = 251351 },
+ { url = "https://files.pythonhosted.org/packages/6f/5c/4d7adc739884f7a9fbe00d1eac8c034023ef8bad71f2ebe12823ca2e3649/multidict-6.6.3-cp312-cp312-win32.whl", hash = "sha256:73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5", size = 41860 },
+ { url = "https://files.pythonhosted.org/packages/6a/a3/0fbc7afdf7cb1aa12a086b02959307848eb6bcc8f66fcb66c0cb57e2a2c1/multidict-6.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17", size = 45982 },
+ { url = "https://files.pythonhosted.org/packages/b8/95/8c825bd70ff9b02462dc18d1295dd08d3e9e4eb66856d292ffa62cfe1920/multidict-6.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b", size = 43210 },
+ { url = "https://files.pythonhosted.org/packages/52/1d/0bebcbbb4f000751fbd09957257903d6e002943fc668d841a4cf2fb7f872/multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55", size = 75843 },
+ { url = "https://files.pythonhosted.org/packages/07/8f/cbe241b0434cfe257f65c2b1bcf9e8d5fb52bc708c5061fb29b0fed22bdf/multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b", size = 45053 },
+ { url = "https://files.pythonhosted.org/packages/32/d2/0b3b23f9dbad5b270b22a3ac3ea73ed0a50ef2d9a390447061178ed6bdb8/multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65", size = 43273 },
+ { url = "https://files.pythonhosted.org/packages/fd/fe/6eb68927e823999e3683bc49678eb20374ba9615097d085298fd5b386564/multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3", size = 237124 },
+ { url = "https://files.pythonhosted.org/packages/e7/ab/320d8507e7726c460cb77117848b3834ea0d59e769f36fdae495f7669929/multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c", size = 256892 },
+ { url = "https://files.pythonhosted.org/packages/76/60/38ee422db515ac69834e60142a1a69111ac96026e76e8e9aa347fd2e4591/multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6", size = 240547 },
+ { url = "https://files.pythonhosted.org/packages/27/fb/905224fde2dff042b030c27ad95a7ae744325cf54b890b443d30a789b80e/multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8", size = 266223 },
+ { url = "https://files.pythonhosted.org/packages/76/35/dc38ab361051beae08d1a53965e3e1a418752fc5be4d3fb983c5582d8784/multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca", size = 267262 },
+ { url = "https://files.pythonhosted.org/packages/1f/a3/0a485b7f36e422421b17e2bbb5a81c1af10eac1d4476f2ff92927c730479/multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884", size = 254345 },
+ { url = "https://files.pythonhosted.org/packages/b4/59/bcdd52c1dab7c0e0d75ff19cac751fbd5f850d1fc39172ce809a74aa9ea4/multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7", size = 252248 },
+ { url = "https://files.pythonhosted.org/packages/bb/a4/2d96aaa6eae8067ce108d4acee6f45ced5728beda55c0f02ae1072c730d1/multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b", size = 250115 },
+ { url = "https://files.pythonhosted.org/packages/25/d2/ed9f847fa5c7d0677d4f02ea2c163d5e48573de3f57bacf5670e43a5ffaa/multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c", size = 249649 },
+ { url = "https://files.pythonhosted.org/packages/1f/af/9155850372563fc550803d3f25373308aa70f59b52cff25854086ecb4a79/multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b", size = 261203 },
+ { url = "https://files.pythonhosted.org/packages/36/2f/c6a728f699896252cf309769089568a33c6439626648843f78743660709d/multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1", size = 258051 },
+ { url = "https://files.pythonhosted.org/packages/d0/60/689880776d6b18fa2b70f6cc74ff87dd6c6b9b47bd9cf74c16fecfaa6ad9/multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6", size = 249601 },
+ { url = "https://files.pythonhosted.org/packages/75/5e/325b11f2222a549019cf2ef879c1f81f94a0d40ace3ef55cf529915ba6cc/multidict-6.6.3-cp313-cp313-win32.whl", hash = "sha256:5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e", size = 41683 },
+ { url = "https://files.pythonhosted.org/packages/b1/ad/cf46e73f5d6e3c775cabd2a05976547f3f18b39bee06260369a42501f053/multidict-6.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9", size = 45811 },
+ { url = "https://files.pythonhosted.org/packages/c5/c9/2e3fe950db28fb7c62e1a5f46e1e38759b072e2089209bc033c2798bb5ec/multidict-6.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600", size = 43056 },
+ { url = "https://files.pythonhosted.org/packages/3a/58/aaf8114cf34966e084a8cc9517771288adb53465188843d5a19862cb6dc3/multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134", size = 82811 },
+ { url = "https://files.pythonhosted.org/packages/71/af/5402e7b58a1f5b987a07ad98f2501fdba2a4f4b4c30cf114e3ce8db64c87/multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37", size = 48304 },
+ { url = "https://files.pythonhosted.org/packages/39/65/ab3c8cafe21adb45b24a50266fd747147dec7847425bc2a0f6934b3ae9ce/multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8", size = 46775 },
+ { url = "https://files.pythonhosted.org/packages/49/ba/9fcc1b332f67cc0c0c8079e263bfab6660f87fe4e28a35921771ff3eea0d/multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1", size = 229773 },
+ { url = "https://files.pythonhosted.org/packages/a4/14/0145a251f555f7c754ce2dcbcd012939bbd1f34f066fa5d28a50e722a054/multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373", size = 250083 },
+ { url = "https://files.pythonhosted.org/packages/9e/d4/d5c0bd2bbb173b586c249a151a26d2fb3ec7d53c96e42091c9fef4e1f10c/multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e", size = 228980 },
+ { url = "https://files.pythonhosted.org/packages/21/32/c9a2d8444a50ec48c4733ccc67254100c10e1c8ae8e40c7a2d2183b59b97/multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f", size = 257776 },
+ { url = "https://files.pythonhosted.org/packages/68/d0/14fa1699f4ef629eae08ad6201c6b476098f5efb051b296f4c26be7a9fdf/multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0", size = 256882 },
+ { url = "https://files.pythonhosted.org/packages/da/88/84a27570fbe303c65607d517a5f147cd2fc046c2d1da02b84b17b9bdc2aa/multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc", size = 247816 },
+ { url = "https://files.pythonhosted.org/packages/1c/60/dca352a0c999ce96a5d8b8ee0b2b9f729dcad2e0b0c195f8286269a2074c/multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f", size = 245341 },
+ { url = "https://files.pythonhosted.org/packages/50/ef/433fa3ed06028f03946f3993223dada70fb700f763f70c00079533c34578/multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471", size = 235854 },
+ { url = "https://files.pythonhosted.org/packages/1b/1f/487612ab56fbe35715320905215a57fede20de7db40a261759690dc80471/multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2", size = 243432 },
+ { url = "https://files.pythonhosted.org/packages/da/6f/ce8b79de16cd885c6f9052c96a3671373d00c59b3ee635ea93e6e81b8ccf/multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648", size = 252731 },
+ { url = "https://files.pythonhosted.org/packages/bb/fe/a2514a6aba78e5abefa1624ca85ae18f542d95ac5cde2e3815a9fbf369aa/multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d", size = 247086 },
+ { url = "https://files.pythonhosted.org/packages/8c/22/b788718d63bb3cce752d107a57c85fcd1a212c6c778628567c9713f9345a/multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c", size = 243338 },
+ { url = "https://files.pythonhosted.org/packages/22/d6/fdb3d0670819f2228f3f7d9af613d5e652c15d170c83e5f1c94fbc55a25b/multidict-6.6.3-cp313-cp313t-win32.whl", hash = "sha256:639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e", size = 47812 },
+ { url = "https://files.pythonhosted.org/packages/b6/d6/a9d2c808f2c489ad199723197419207ecbfbc1776f6e155e1ecea9c883aa/multidict-6.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d", size = 53011 },
+ { url = "https://files.pythonhosted.org/packages/f2/40/b68001cba8188dd267590a111f9661b6256debc327137667e832bf5d66e8/multidict-6.6.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb", size = 45254 },
+ { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313 },
+]
+
+[[package]]
+name = "nano-vectordb"
+version = "0.0.4.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cb/ff/ed9ff1c4e5b0418687c17d02fdc453c212e7550c62622914ba0243c106bc/nano_vectordb-0.0.4.3.tar.gz", hash = "sha256:3d13074476f2b739e51261974ed44aa467725579966219734c03502c929ed3b5", size = 6332 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/d8/f1876f59916da0a2147e63066650c46bf7992828a9e92f1b4e3b695f1fb0/nano_vectordb-0.0.4.3-py3-none-any.whl", hash = "sha256:1b70401a54c02fabf76515b5dfb630076434547ed3c6861828ee8771b6dd7c19", size = 5590 },
+]
+
+[[package]]
+name = "networkx"
+version = "3.4.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263 },
+]
+
+[[package]]
+name = "networkx"
+version = "3.5"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406 },
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.9.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 },
+]
+
+[[package]]
+name = "numpy"
+version = "2.2.6"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245 },
+ { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048 },
+ { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542 },
+ { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301 },
+ { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320 },
+ { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050 },
+ { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034 },
+ { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185 },
+ { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149 },
+ { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620 },
+ { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963 },
+ { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743 },
+ { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616 },
+ { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579 },
+ { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005 },
+ { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570 },
+ { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548 },
+ { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521 },
+ { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866 },
+ { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455 },
+ { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348 },
+ { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362 },
+ { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103 },
+ { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382 },
+ { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462 },
+ { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618 },
+ { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511 },
+ { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783 },
+ { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506 },
+ { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190 },
+ { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828 },
+ { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006 },
+ { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765 },
+ { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736 },
+ { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719 },
+ { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072 },
+ { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213 },
+ { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632 },
+ { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 },
+ { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 },
+ { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467 },
+ { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144 },
+ { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217 },
+ { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014 },
+ { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935 },
+ { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122 },
+ { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143 },
+ { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260 },
+ { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 },
+ { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 },
+ { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391 },
+ { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754 },
+ { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476 },
+ { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666 },
+]
+
+[[package]]
+name = "numpy"
+version = "2.3.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2e/19/d7c972dfe90a353dbd3efbbe1d14a5951de80c99c9dc1b93cd998d51dc0f/numpy-2.3.1.tar.gz", hash = "sha256:1ec9ae20a4226da374362cca3c62cd753faf2f951440b0e3b98e93c235441d2b", size = 20390372 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b0/c7/87c64d7ab426156530676000c94784ef55676df2f13b2796f97722464124/numpy-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ea9e48336a402551f52cd8f593343699003d2353daa4b72ce8d34f66b722070", size = 21199346 },
+ { url = "https://files.pythonhosted.org/packages/58/0e/0966c2f44beeac12af8d836e5b5f826a407cf34c45cb73ddcdfce9f5960b/numpy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ccb7336eaf0e77c1635b232c141846493a588ec9ea777a7c24d7166bb8533ae", size = 14361143 },
+ { url = "https://files.pythonhosted.org/packages/7d/31/6e35a247acb1bfc19226791dfc7d4c30002cd4e620e11e58b0ddf836fe52/numpy-2.3.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0bb3a4a61e1d327e035275d2a993c96fa786e4913aa089843e6a2d9dd205c66a", size = 5378989 },
+ { url = "https://files.pythonhosted.org/packages/b0/25/93b621219bb6f5a2d4e713a824522c69ab1f06a57cd571cda70e2e31af44/numpy-2.3.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:e344eb79dab01f1e838ebb67aab09965fb271d6da6b00adda26328ac27d4a66e", size = 6912890 },
+ { url = "https://files.pythonhosted.org/packages/ef/60/6b06ed98d11fb32e27fb59468b42383f3877146d3ee639f733776b6ac596/numpy-2.3.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:467db865b392168ceb1ef1ffa6f5a86e62468c43e0cfb4ab6da667ede10e58db", size = 14569032 },
+ { url = "https://files.pythonhosted.org/packages/75/c9/9bec03675192077467a9c7c2bdd1f2e922bd01d3a69b15c3a0fdcd8548f6/numpy-2.3.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:afed2ce4a84f6b0fc6c1ce734ff368cbf5a5e24e8954a338f3bdffa0718adffb", size = 16930354 },
+ { url = "https://files.pythonhosted.org/packages/6a/e2/5756a00cabcf50a3f527a0c968b2b4881c62b1379223931853114fa04cda/numpy-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0025048b3c1557a20bc80d06fdeb8cc7fc193721484cca82b2cfa072fec71a93", size = 15879605 },
+ { url = "https://files.pythonhosted.org/packages/ff/86/a471f65f0a86f1ca62dcc90b9fa46174dd48f50214e5446bc16a775646c5/numpy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5ee121b60aa509679b682819c602579e1df14a5b07fe95671c8849aad8f2115", size = 18666994 },
+ { url = "https://files.pythonhosted.org/packages/43/a6/482a53e469b32be6500aaf61cfafd1de7a0b0d484babf679209c3298852e/numpy-2.3.1-cp311-cp311-win32.whl", hash = "sha256:a8b740f5579ae4585831b3cf0e3b0425c667274f82a484866d2adf9570539369", size = 6603672 },
+ { url = "https://files.pythonhosted.org/packages/6b/fb/bb613f4122c310a13ec67585c70e14b03bfc7ebabd24f4d5138b97371d7c/numpy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4580adadc53311b163444f877e0789f1c8861e2698f6b2a4ca852fda154f3ff", size = 13024015 },
+ { url = "https://files.pythonhosted.org/packages/51/58/2d842825af9a0c041aca246dc92eb725e1bc5e1c9ac89712625db0c4e11c/numpy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:ec0bdafa906f95adc9a0c6f26a4871fa753f25caaa0e032578a30457bff0af6a", size = 10456989 },
+ { url = "https://files.pythonhosted.org/packages/c6/56/71ad5022e2f63cfe0ca93559403d0edef14aea70a841d640bd13cdba578e/numpy-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2959d8f268f3d8ee402b04a9ec4bb7604555aeacf78b360dc4ec27f1d508177d", size = 20896664 },
+ { url = "https://files.pythonhosted.org/packages/25/65/2db52ba049813670f7f987cc5db6dac9be7cd95e923cc6832b3d32d87cef/numpy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:762e0c0c6b56bdedfef9a8e1d4538556438288c4276901ea008ae44091954e29", size = 14131078 },
+ { url = "https://files.pythonhosted.org/packages/57/dd/28fa3c17b0e751047ac928c1e1b6990238faad76e9b147e585b573d9d1bd/numpy-2.3.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:867ef172a0976aaa1f1d1b63cf2090de8b636a7674607d514505fb7276ab08fc", size = 5112554 },
+ { url = "https://files.pythonhosted.org/packages/c9/fc/84ea0cba8e760c4644b708b6819d91784c290288c27aca916115e3311d17/numpy-2.3.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:4e602e1b8682c2b833af89ba641ad4176053aaa50f5cacda1a27004352dde943", size = 6646560 },
+ { url = "https://files.pythonhosted.org/packages/61/b2/512b0c2ddec985ad1e496b0bd853eeb572315c0f07cd6997473ced8f15e2/numpy-2.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8e333040d069eba1652fb08962ec5b76af7f2c7bce1df7e1418c8055cf776f25", size = 14260638 },
+ { url = "https://files.pythonhosted.org/packages/6e/45/c51cb248e679a6c6ab14b7a8e3ead3f4a3fe7425fc7a6f98b3f147bec532/numpy-2.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e7cbf5a5eafd8d230a3ce356d892512185230e4781a361229bd902ff403bc660", size = 16632729 },
+ { url = "https://files.pythonhosted.org/packages/e4/ff/feb4be2e5c09a3da161b412019caf47183099cbea1132fd98061808c2df2/numpy-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1b8f26d1086835f442286c1d9b64bb3974b0b1e41bb105358fd07d20872952", size = 15565330 },
+ { url = "https://files.pythonhosted.org/packages/bc/6d/ceafe87587101e9ab0d370e4f6e5f3f3a85b9a697f2318738e5e7e176ce3/numpy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ee8340cb48c9b7a5899d1149eece41ca535513a9698098edbade2a8e7a84da77", size = 18361734 },
+ { url = "https://files.pythonhosted.org/packages/2b/19/0fb49a3ea088be691f040c9bf1817e4669a339d6e98579f91859b902c636/numpy-2.3.1-cp312-cp312-win32.whl", hash = "sha256:e772dda20a6002ef7061713dc1e2585bc1b534e7909b2030b5a46dae8ff077ab", size = 6320411 },
+ { url = "https://files.pythonhosted.org/packages/b1/3e/e28f4c1dd9e042eb57a3eb652f200225e311b608632bc727ae378623d4f8/numpy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfecc7822543abdea6de08758091da655ea2210b8ffa1faf116b940693d3df76", size = 12734973 },
+ { url = "https://files.pythonhosted.org/packages/04/a8/8a5e9079dc722acf53522b8f8842e79541ea81835e9b5483388701421073/numpy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:7be91b2239af2658653c5bb6f1b8bccafaf08226a258caf78ce44710a0160d30", size = 10191491 },
+ { url = "https://files.pythonhosted.org/packages/d4/bd/35ad97006d8abff8631293f8ea6adf07b0108ce6fec68da3c3fcca1197f2/numpy-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25a1992b0a3fdcdaec9f552ef10d8103186f5397ab45e2d25f8ac51b1a6b97e8", size = 20889381 },
+ { url = "https://files.pythonhosted.org/packages/f1/4f/df5923874d8095b6062495b39729178eef4a922119cee32a12ee1bd4664c/numpy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dea630156d39b02a63c18f508f85010230409db5b2927ba59c8ba4ab3e8272e", size = 14152726 },
+ { url = "https://files.pythonhosted.org/packages/8c/0f/a1f269b125806212a876f7efb049b06c6f8772cf0121139f97774cd95626/numpy-2.3.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bada6058dd886061f10ea15f230ccf7dfff40572e99fef440a4a857c8728c9c0", size = 5105145 },
+ { url = "https://files.pythonhosted.org/packages/6d/63/a7f7fd5f375b0361682f6ffbf686787e82b7bbd561268e4f30afad2bb3c0/numpy-2.3.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:a894f3816eb17b29e4783e5873f92faf55b710c2519e5c351767c51f79d8526d", size = 6639409 },
+ { url = "https://files.pythonhosted.org/packages/bf/0d/1854a4121af895aab383f4aa233748f1df4671ef331d898e32426756a8a6/numpy-2.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:18703df6c4a4fee55fd3d6e5a253d01c5d33a295409b03fda0c86b3ca2ff41a1", size = 14257630 },
+ { url = "https://files.pythonhosted.org/packages/50/30/af1b277b443f2fb08acf1c55ce9d68ee540043f158630d62cef012750f9f/numpy-2.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5902660491bd7a48b2ec16c23ccb9124b8abfd9583c5fdfa123fe6b421e03de1", size = 16627546 },
+ { url = "https://files.pythonhosted.org/packages/6e/ec/3b68220c277e463095342d254c61be8144c31208db18d3fd8ef02712bcd6/numpy-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:36890eb9e9d2081137bd78d29050ba63b8dab95dff7912eadf1185e80074b2a0", size = 15562538 },
+ { url = "https://files.pythonhosted.org/packages/77/2b/4014f2bcc4404484021c74d4c5ee8eb3de7e3f7ac75f06672f8dcf85140a/numpy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a780033466159c2270531e2b8ac063704592a0bc62ec4a1b991c7c40705eb0e8", size = 18360327 },
+ { url = "https://files.pythonhosted.org/packages/40/8d/2ddd6c9b30fcf920837b8672f6c65590c7d92e43084c25fc65edc22e93ca/numpy-2.3.1-cp313-cp313-win32.whl", hash = "sha256:39bff12c076812595c3a306f22bfe49919c5513aa1e0e70fac756a0be7c2a2b8", size = 6312330 },
+ { url = "https://files.pythonhosted.org/packages/dd/c8/beaba449925988d415efccb45bf977ff8327a02f655090627318f6398c7b/numpy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d5ee6eec45f08ce507a6570e06f2f879b374a552087a4179ea7838edbcbfa42", size = 12731565 },
+ { url = "https://files.pythonhosted.org/packages/0b/c3/5c0c575d7ec78c1126998071f58facfc124006635da75b090805e642c62e/numpy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c4d9e0a8368db90f93bd192bfa771ace63137c3488d198ee21dfb8e7771916e", size = 10190262 },
+ { url = "https://files.pythonhosted.org/packages/ea/19/a029cd335cf72f79d2644dcfc22d90f09caa86265cbbde3b5702ccef6890/numpy-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b0b5397374f32ec0649dd98c652a1798192042e715df918c20672c62fb52d4b8", size = 20987593 },
+ { url = "https://files.pythonhosted.org/packages/25/91/8ea8894406209107d9ce19b66314194675d31761fe2cb3c84fe2eeae2f37/numpy-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c5bdf2015ccfcee8253fb8be695516ac4457c743473a43290fd36eba6a1777eb", size = 14300523 },
+ { url = "https://files.pythonhosted.org/packages/a6/7f/06187b0066eefc9e7ce77d5f2ddb4e314a55220ad62dd0bfc9f2c44bac14/numpy-2.3.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d70f20df7f08b90a2062c1f07737dd340adccf2068d0f1b9b3d56e2038979fee", size = 5227993 },
+ { url = "https://files.pythonhosted.org/packages/e8/ec/a926c293c605fa75e9cfb09f1e4840098ed46d2edaa6e2152ee35dc01ed3/numpy-2.3.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:2fb86b7e58f9ac50e1e9dd1290154107e47d1eef23a0ae9145ded06ea606f992", size = 6736652 },
+ { url = "https://files.pythonhosted.org/packages/e3/62/d68e52fb6fde5586650d4c0ce0b05ff3a48ad4df4ffd1b8866479d1d671d/numpy-2.3.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:23ab05b2d241f76cb883ce8b9a93a680752fbfcbd51c50eff0b88b979e471d8c", size = 14331561 },
+ { url = "https://files.pythonhosted.org/packages/fc/ec/b74d3f2430960044bdad6900d9f5edc2dc0fb8bf5a0be0f65287bf2cbe27/numpy-2.3.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ce2ce9e5de4703a673e705183f64fd5da5bf36e7beddcb63a25ee2286e71ca48", size = 16693349 },
+ { url = "https://files.pythonhosted.org/packages/0d/15/def96774b9d7eb198ddadfcbd20281b20ebb510580419197e225f5c55c3e/numpy-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c4913079974eeb5c16ccfd2b1f09354b8fed7e0d6f2cab933104a09a6419b1ee", size = 15642053 },
+ { url = "https://files.pythonhosted.org/packages/2b/57/c3203974762a759540c6ae71d0ea2341c1fa41d84e4971a8e76d7141678a/numpy-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:010ce9b4f00d5c036053ca684c77441f2f2c934fd23bee058b4d6f196efd8280", size = 18434184 },
+ { url = "https://files.pythonhosted.org/packages/22/8a/ccdf201457ed8ac6245187850aff4ca56a79edbea4829f4e9f14d46fa9a5/numpy-2.3.1-cp313-cp313t-win32.whl", hash = "sha256:6269b9edfe32912584ec496d91b00b6d34282ca1d07eb10e82dfc780907d6c2e", size = 6440678 },
+ { url = "https://files.pythonhosted.org/packages/f1/7e/7f431d8bd8eb7e03d79294aed238b1b0b174b3148570d03a8a8a8f6a0da9/numpy-2.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2a809637460e88a113e186e87f228d74ae2852a2e0c44de275263376f17b5bdc", size = 12870697 },
+ { url = "https://files.pythonhosted.org/packages/d4/ca/af82bf0fad4c3e573c6930ed743b5308492ff19917c7caaf2f9b6f9e2e98/numpy-2.3.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eccb9a159db9aed60800187bc47a6d3451553f0e1b08b068d8b277ddfbb9b244", size = 10260376 },
+ { url = "https://files.pythonhosted.org/packages/e8/34/facc13b9b42ddca30498fc51f7f73c3d0f2be179943a4b4da8686e259740/numpy-2.3.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ad506d4b09e684394c42c966ec1527f6ebc25da7f4da4b1b056606ffe446b8a3", size = 21070637 },
+ { url = "https://files.pythonhosted.org/packages/65/b6/41b705d9dbae04649b529fc9bd3387664c3281c7cd78b404a4efe73dcc45/numpy-2.3.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ebb8603d45bc86bbd5edb0d63e52c5fd9e7945d3a503b77e486bd88dde67a19b", size = 5304087 },
+ { url = "https://files.pythonhosted.org/packages/7a/b4/fe3ac1902bff7a4934a22d49e1c9d71a623204d654d4cc43c6e8fe337fcb/numpy-2.3.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:15aa4c392ac396e2ad3d0a2680c0f0dee420f9fed14eef09bdb9450ee6dcb7b7", size = 6817588 },
+ { url = "https://files.pythonhosted.org/packages/ae/ee/89bedf69c36ace1ac8f59e97811c1f5031e179a37e4821c3a230bf750142/numpy-2.3.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c6e0bf9d1a2f50d2b65a7cf56db37c095af17b59f6c132396f7c6d5dd76484df", size = 14399010 },
+ { url = "https://files.pythonhosted.org/packages/15/08/e00e7070ede29b2b176165eba18d6f9784d5349be3c0c1218338e79c27fd/numpy-2.3.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eabd7e8740d494ce2b4ea0ff05afa1b7b291e978c0ae075487c51e8bd93c0c68", size = 16752042 },
+ { url = "https://files.pythonhosted.org/packages/48/6b/1c6b515a83d5564b1698a61efa245727c8feecf308f4091f565988519d20/numpy-2.3.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e610832418a2bc09d974cc9fecebfa51e9532d6190223bc5ef6a7402ebf3b5cb", size = 12927246 },
+]
+
+[[package]]
+name = "openai"
+version = "1.93.3"
+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/e0/66/fadc0cad6a229c6a85c3aa5f222a786ec4d9bf14c2a004f80ffa21dbaf21/openai-1.93.3.tar.gz", hash = "sha256:488b76399238c694af7e4e30c58170ea55e6f65038ab27dbe95b5077a00f8af8", size = 487595 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/b9/0df6351b25c6bd494c534d2a8191dc9460fb5bb09c88b1427775d49fde05/openai-1.93.3-py3-none-any.whl", hash = "sha256:41aaa7594c7d141b46eed0a58dcd75d20edcc809fdd2c931ecbb4957dc98a892", size = 755132 },
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
+]
+
+[[package]]
+name = "pandas"
+version = "2.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "python-dateutil" },
+ { name = "pytz" },
+ { name = "tzdata" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c4/ca/aa97b47287221fa37a49634532e520300088e290b20d690b21ce3e448143/pandas-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22c2e866f7209ebc3a8f08d75766566aae02bcc91d196935a1d9e59c7b990ac9", size = 11542731 },
+ { url = "https://files.pythonhosted.org/packages/80/bf/7938dddc5f01e18e573dcfb0f1b8c9357d9b5fa6ffdee6e605b92efbdff2/pandas-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3583d348546201aff730c8c47e49bc159833f971c2899d6097bce68b9112a4f1", size = 10790031 },
+ { url = "https://files.pythonhosted.org/packages/ee/2f/9af748366763b2a494fed477f88051dbf06f56053d5c00eba652697e3f94/pandas-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f951fbb702dacd390561e0ea45cdd8ecfa7fb56935eb3dd78e306c19104b9b0", size = 11724083 },
+ { url = "https://files.pythonhosted.org/packages/2c/95/79ab37aa4c25d1e7df953dde407bb9c3e4ae47d154bc0dd1692f3a6dcf8c/pandas-2.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd05b72ec02ebfb993569b4931b2e16fbb4d6ad6ce80224a3ee838387d83a191", size = 12342360 },
+ { url = "https://files.pythonhosted.org/packages/75/a7/d65e5d8665c12c3c6ff5edd9709d5836ec9b6f80071b7f4a718c6106e86e/pandas-2.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1b916a627919a247d865aed068eb65eb91a344b13f5b57ab9f610b7716c92de1", size = 13202098 },
+ { url = "https://files.pythonhosted.org/packages/65/f3/4c1dbd754dbaa79dbf8b537800cb2fa1a6e534764fef50ab1f7533226c5c/pandas-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fe67dc676818c186d5a3d5425250e40f179c2a89145df477dd82945eaea89e97", size = 13837228 },
+ { url = "https://files.pythonhosted.org/packages/3f/d6/d7f5777162aa9b48ec3910bca5a58c9b5927cfd9cfde3aa64322f5ba4b9f/pandas-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:2eb789ae0274672acbd3c575b0598d213345660120a257b47b5dafdc618aec83", size = 11336561 },
+ { url = "https://files.pythonhosted.org/packages/76/1c/ccf70029e927e473a4476c00e0d5b32e623bff27f0402d0a92b7fc29bb9f/pandas-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2b0540963d83431f5ce8870ea02a7430adca100cec8a050f0811f8e31035541b", size = 11566608 },
+ { url = "https://files.pythonhosted.org/packages/ec/d3/3c37cb724d76a841f14b8f5fe57e5e3645207cc67370e4f84717e8bb7657/pandas-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fe7317f578c6a153912bd2292f02e40c1d8f253e93c599e82620c7f69755c74f", size = 10823181 },
+ { url = "https://files.pythonhosted.org/packages/8a/4c/367c98854a1251940edf54a4df0826dcacfb987f9068abf3e3064081a382/pandas-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6723a27ad7b244c0c79d8e7007092d7c8f0f11305770e2f4cd778b3ad5f9f85", size = 11793570 },
+ { url = "https://files.pythonhosted.org/packages/07/5f/63760ff107bcf5146eee41b38b3985f9055e710a72fdd637b791dea3495c/pandas-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3462c3735fe19f2638f2c3a40bd94ec2dc5ba13abbb032dd2fa1f540a075509d", size = 12378887 },
+ { url = "https://files.pythonhosted.org/packages/15/53/f31a9b4dfe73fe4711c3a609bd8e60238022f48eacedc257cd13ae9327a7/pandas-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:98bcc8b5bf7afed22cc753a28bc4d9e26e078e777066bc53fac7904ddef9a678", size = 13230957 },
+ { url = "https://files.pythonhosted.org/packages/e0/94/6fce6bf85b5056d065e0a7933cba2616dcb48596f7ba3c6341ec4bcc529d/pandas-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d544806b485ddf29e52d75b1f559142514e60ef58a832f74fb38e48d757b299", size = 13883883 },
+ { url = "https://files.pythonhosted.org/packages/c8/7b/bdcb1ed8fccb63d04bdb7635161d0ec26596d92c9d7a6cce964e7876b6c1/pandas-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b3cd4273d3cb3707b6fffd217204c52ed92859533e31dc03b7c5008aa933aaab", size = 11340212 },
+ { url = "https://files.pythonhosted.org/packages/46/de/b8445e0f5d217a99fe0eeb2f4988070908979bec3587c0633e5428ab596c/pandas-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:689968e841136f9e542020698ee1c4fbe9caa2ed2213ae2388dc7b81721510d3", size = 11588172 },
+ { url = "https://files.pythonhosted.org/packages/1e/e0/801cdb3564e65a5ac041ab99ea6f1d802a6c325bb6e58c79c06a3f1cd010/pandas-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:025e92411c16cbe5bb2a4abc99732a6b132f439b8aab23a59fa593eb00704232", size = 10717365 },
+ { url = "https://files.pythonhosted.org/packages/51/a5/c76a8311833c24ae61a376dbf360eb1b1c9247a5d9c1e8b356563b31b80c/pandas-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7ff55f31c4fcb3e316e8f7fa194566b286d6ac430afec0d461163312c5841e", size = 11280411 },
+ { url = "https://files.pythonhosted.org/packages/da/01/e383018feba0a1ead6cf5fe8728e5d767fee02f06a3d800e82c489e5daaf/pandas-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dcb79bf373a47d2a40cf7232928eb7540155abbc460925c2c96d2d30b006eb4", size = 11988013 },
+ { url = "https://files.pythonhosted.org/packages/5b/14/cec7760d7c9507f11c97d64f29022e12a6cc4fc03ac694535e89f88ad2ec/pandas-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56a342b231e8862c96bdb6ab97170e203ce511f4d0429589c8ede1ee8ece48b8", size = 12767210 },
+ { url = "https://files.pythonhosted.org/packages/50/b9/6e2d2c6728ed29fb3d4d4d302504fb66f1a543e37eb2e43f352a86365cdf/pandas-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ca7ed14832bce68baef331f4d7f294411bed8efd032f8109d690df45e00c4679", size = 13440571 },
+ { url = "https://files.pythonhosted.org/packages/80/a5/3a92893e7399a691bad7664d977cb5e7c81cf666c81f89ea76ba2bff483d/pandas-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ac942bfd0aca577bef61f2bc8da8147c4ef6879965ef883d8e8d5d2dc3e744b8", size = 10987601 },
+ { url = "https://files.pythonhosted.org/packages/32/ed/ff0a67a2c5505e1854e6715586ac6693dd860fbf52ef9f81edee200266e7/pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22", size = 11531393 },
+ { url = "https://files.pythonhosted.org/packages/c7/db/d8f24a7cc9fb0972adab0cc80b6817e8bef888cfd0024eeb5a21c0bb5c4a/pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a", size = 10668750 },
+ { url = "https://files.pythonhosted.org/packages/0f/b0/80f6ec783313f1e2356b28b4fd8d2148c378370045da918c73145e6aab50/pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928", size = 11342004 },
+ { url = "https://files.pythonhosted.org/packages/e9/e2/20a317688435470872885e7fc8f95109ae9683dec7c50be29b56911515a5/pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9", size = 12050869 },
+ { url = "https://files.pythonhosted.org/packages/55/79/20d746b0a96c67203a5bee5fb4e00ac49c3e8009a39e1f78de264ecc5729/pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12", size = 12750218 },
+ { url = "https://files.pythonhosted.org/packages/7c/0f/145c8b41e48dbf03dd18fdd7f24f8ba95b8254a97a3379048378f33e7838/pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb", size = 13416763 },
+ { url = "https://files.pythonhosted.org/packages/b2/c0/54415af59db5cdd86a3d3bf79863e8cc3fa9ed265f0745254061ac09d5f2/pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956", size = 10987482 },
+ { url = "https://files.pythonhosted.org/packages/48/64/2fd2e400073a1230e13b8cd604c9bc95d9e3b962e5d44088ead2e8f0cfec/pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a", size = 12029159 },
+ { url = "https://files.pythonhosted.org/packages/d8/0a/d84fd79b0293b7ef88c760d7dca69828d867c89b6d9bc52d6a27e4d87316/pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9", size = 11393287 },
+ { url = "https://files.pythonhosted.org/packages/50/ae/ff885d2b6e88f3c7520bb74ba319268b42f05d7e583b5dded9837da2723f/pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275", size = 11309381 },
+ { url = "https://files.pythonhosted.org/packages/85/86/1fa345fc17caf5d7780d2699985c03dbe186c68fee00b526813939062bb0/pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab", size = 11883998 },
+ { url = "https://files.pythonhosted.org/packages/81/aa/e58541a49b5e6310d89474333e994ee57fea97c8aaa8fc7f00b873059bbf/pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96", size = 12704705 },
+ { url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044 },
+]
+
+[[package]]
+name = "passlib"
+version = "1.7.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554 },
+]
+
+[package.optional-dependencies]
+bcrypt = [
+ { name = "bcrypt" },
+]
+
+[[package]]
+name = "pipmaster"
+version = "0.9.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ascii-colors" },
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3e/88/74ddd8c3e4b1614e5eb743fbfc020a6ecd60bbcd24bbe74cb4bcf03dc644/pipmaster-0.9.2.tar.gz", hash = "sha256:685ab68f85d607bda2c66648ac7f7eb1f0862979765f78ab4e02d56390d1b5fa", size = 33347 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f8/e3/140283c907d9abf17e109cbca04e04d8d742e2fe2c60d23e09c0fd2c072c/pipmaster-0.9.2-py3-none-any.whl", hash = "sha256:44c21dfd9267ad0e19a1a56f71980457e8a51789cf193a794b3c00d6b0f83b53", size = 25070 },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.3.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 },
+]
+
+[[package]]
+name = "pre-commit"
+version = "4.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cfgv" },
+ { name = "identify" },
+ { name = "nodeenv" },
+ { name = "pyyaml" },
+ { name = "virtualenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 },
+]
+
+[[package]]
+name = "propcache"
+version = "0.3.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ab/14/510deed325e262afeb8b360043c5d7c960da7d3ecd6d6f9496c9c56dc7f4/propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770", size = 73178 },
+ { url = "https://files.pythonhosted.org/packages/cd/4e/ad52a7925ff01c1325653a730c7ec3175a23f948f08626a534133427dcff/propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3", size = 43133 },
+ { url = "https://files.pythonhosted.org/packages/63/7c/e9399ba5da7780871db4eac178e9c2e204c23dd3e7d32df202092a1ed400/propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3", size = 43039 },
+ { url = "https://files.pythonhosted.org/packages/22/e1/58da211eb8fdc6fc854002387d38f415a6ca5f5c67c1315b204a5d3e9d7a/propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e", size = 201903 },
+ { url = "https://files.pythonhosted.org/packages/c4/0a/550ea0f52aac455cb90111c8bab995208443e46d925e51e2f6ebdf869525/propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220", size = 213362 },
+ { url = "https://files.pythonhosted.org/packages/5a/af/9893b7d878deda9bb69fcf54600b247fba7317761b7db11fede6e0f28bd0/propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb", size = 210525 },
+ { url = "https://files.pythonhosted.org/packages/7c/bb/38fd08b278ca85cde36d848091ad2b45954bc5f15cce494bb300b9285831/propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614", size = 198283 },
+ { url = "https://files.pythonhosted.org/packages/78/8c/9fe55bd01d362bafb413dfe508c48753111a1e269737fa143ba85693592c/propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50", size = 191872 },
+ { url = "https://files.pythonhosted.org/packages/54/14/4701c33852937a22584e08abb531d654c8bcf7948a8f87ad0a4822394147/propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339", size = 199452 },
+ { url = "https://files.pythonhosted.org/packages/16/44/447f2253d859602095356007657ee535e0093215ea0b3d1d6a41d16e5201/propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0", size = 191567 },
+ { url = "https://files.pythonhosted.org/packages/f2/b3/e4756258749bb2d3b46defcff606a2f47410bab82be5824a67e84015b267/propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2", size = 193015 },
+ { url = "https://files.pythonhosted.org/packages/1e/df/e6d3c7574233164b6330b9fd697beeac402afd367280e6dc377bb99b43d9/propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7", size = 204660 },
+ { url = "https://files.pythonhosted.org/packages/b2/53/e4d31dd5170b4a0e2e6b730f2385a96410633b4833dc25fe5dffd1f73294/propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b", size = 206105 },
+ { url = "https://files.pythonhosted.org/packages/7f/fe/74d54cf9fbe2a20ff786e5f7afcfde446588f0cf15fb2daacfbc267b866c/propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c", size = 196980 },
+ { url = "https://files.pythonhosted.org/packages/22/ec/c469c9d59dada8a7679625e0440b544fe72e99311a4679c279562051f6fc/propcache-0.3.2-cp310-cp310-win32.whl", hash = "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70", size = 37679 },
+ { url = "https://files.pythonhosted.org/packages/38/35/07a471371ac89d418f8d0b699c75ea6dca2041fbda360823de21f6a9ce0a/propcache-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9", size = 41459 },
+ { url = "https://files.pythonhosted.org/packages/80/8d/e8b436717ab9c2cfc23b116d2c297305aa4cd8339172a456d61ebf5669b8/propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be", size = 74207 },
+ { url = "https://files.pythonhosted.org/packages/d6/29/1e34000e9766d112171764b9fa3226fa0153ab565d0c242c70e9945318a7/propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f", size = 43648 },
+ { url = "https://files.pythonhosted.org/packages/46/92/1ad5af0df781e76988897da39b5f086c2bf0f028b7f9bd1f409bb05b6874/propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9", size = 43496 },
+ { url = "https://files.pythonhosted.org/packages/b3/ce/e96392460f9fb68461fabab3e095cb00c8ddf901205be4eae5ce246e5b7e/propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf", size = 217288 },
+ { url = "https://files.pythonhosted.org/packages/c5/2a/866726ea345299f7ceefc861a5e782b045545ae6940851930a6adaf1fca6/propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9", size = 227456 },
+ { url = "https://files.pythonhosted.org/packages/de/03/07d992ccb6d930398689187e1b3c718339a1c06b8b145a8d9650e4726166/propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66", size = 225429 },
+ { url = "https://files.pythonhosted.org/packages/5d/e6/116ba39448753b1330f48ab8ba927dcd6cf0baea8a0ccbc512dfb49ba670/propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df", size = 213472 },
+ { url = "https://files.pythonhosted.org/packages/a6/85/f01f5d97e54e428885a5497ccf7f54404cbb4f906688a1690cd51bf597dc/propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2", size = 204480 },
+ { url = "https://files.pythonhosted.org/packages/e3/79/7bf5ab9033b8b8194cc3f7cf1aaa0e9c3256320726f64a3e1f113a812dce/propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7", size = 214530 },
+ { url = "https://files.pythonhosted.org/packages/31/0b/bd3e0c00509b609317df4a18e6b05a450ef2d9a963e1d8bc9c9415d86f30/propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95", size = 205230 },
+ { url = "https://files.pythonhosted.org/packages/7a/23/fae0ff9b54b0de4e819bbe559508da132d5683c32d84d0dc2ccce3563ed4/propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e", size = 206754 },
+ { url = "https://files.pythonhosted.org/packages/b7/7f/ad6a3c22630aaa5f618b4dc3c3598974a72abb4c18e45a50b3cdd091eb2f/propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e", size = 218430 },
+ { url = "https://files.pythonhosted.org/packages/5b/2c/ba4f1c0e8a4b4c75910742f0d333759d441f65a1c7f34683b4a74c0ee015/propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf", size = 223884 },
+ { url = "https://files.pythonhosted.org/packages/88/e4/ebe30fc399e98572019eee82ad0caf512401661985cbd3da5e3140ffa1b0/propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e", size = 211480 },
+ { url = "https://files.pythonhosted.org/packages/96/0a/7d5260b914e01d1d0906f7f38af101f8d8ed0dc47426219eeaf05e8ea7c2/propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897", size = 37757 },
+ { url = "https://files.pythonhosted.org/packages/e1/2d/89fe4489a884bc0da0c3278c552bd4ffe06a1ace559db5ef02ef24ab446b/propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39", size = 41500 },
+ { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674 },
+ { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570 },
+ { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094 },
+ { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958 },
+ { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894 },
+ { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672 },
+ { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395 },
+ { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510 },
+ { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949 },
+ { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258 },
+ { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036 },
+ { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684 },
+ { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562 },
+ { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142 },
+ { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711 },
+ { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479 },
+ { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286 },
+ { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425 },
+ { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846 },
+ { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871 },
+ { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720 },
+ { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203 },
+ { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365 },
+ { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016 },
+ { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596 },
+ { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977 },
+ { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220 },
+ { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642 },
+ { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789 },
+ { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880 },
+ { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220 },
+ { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678 },
+ { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560 },
+ { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676 },
+ { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701 },
+ { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934 },
+ { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316 },
+ { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619 },
+ { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896 },
+ { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111 },
+ { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334 },
+ { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026 },
+ { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724 },
+ { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868 },
+ { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322 },
+ { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778 },
+ { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175 },
+ { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857 },
+ { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663 },
+]
+
+[[package]]
+name = "psutil"
+version = "7.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b3/31/4723d756b59344b643542936e37a31d1d3204bcdc42a7daa8ee9eb06fb50/psutil-7.1.0.tar.gz", hash = "sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2", size = 497660 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/46/62/ce4051019ee20ce0ed74432dd73a5bb087a6704284a470bb8adff69a0932/psutil-7.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13", size = 245242 },
+ { url = "https://files.pythonhosted.org/packages/38/61/f76959fba841bf5b61123fbf4b650886dc4094c6858008b5bf73d9057216/psutil-7.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5", size = 246682 },
+ { url = "https://files.pythonhosted.org/packages/88/7a/37c99d2e77ec30d63398ffa6a660450b8a62517cabe44b3e9bae97696e8d/psutil-7.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3", size = 287994 },
+ { url = "https://files.pythonhosted.org/packages/9d/de/04c8c61232f7244aa0a4b9a9fbd63a89d5aeaf94b2fc9d1d16e2faa5cbb0/psutil-7.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3", size = 291163 },
+ { url = "https://files.pythonhosted.org/packages/f4/58/c4f976234bf6d4737bc8c02a81192f045c307b72cf39c9e5c5a2d78927f6/psutil-7.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d", size = 293625 },
+ { url = "https://files.pythonhosted.org/packages/79/87/157c8e7959ec39ced1b11cc93c730c4fb7f9d408569a6c59dbd92ceb35db/psutil-7.1.0-cp37-abi3-win32.whl", hash = "sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca", size = 244812 },
+ { url = "https://files.pythonhosted.org/packages/bf/e9/b44c4f697276a7a95b8e94d0e320a7bf7f3318521b23de69035540b39838/psutil-7.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d", size = 247965 },
+ { url = "https://files.pythonhosted.org/packages/26/65/1070a6e3c036f39142c2820c4b52e9243246fcfc3f96239ac84472ba361e/psutil-7.1.0-cp37-abi3-win_arm64.whl", hash = "sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07", size = 244971 },
+]
+
+[[package]]
+name = "pyasn1"
+version = "0.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 },
+]
+
+[[package]]
+name = "pycparser"
+version = "2.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.11.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782 },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.33.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817 },
+ { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357 },
+ { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011 },
+ { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730 },
+ { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178 },
+ { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462 },
+ { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652 },
+ { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306 },
+ { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720 },
+ { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915 },
+ { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884 },
+ { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496 },
+ { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019 },
+ { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584 },
+ { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071 },
+ { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823 },
+ { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792 },
+ { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338 },
+ { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998 },
+ { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200 },
+ { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890 },
+ { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359 },
+ { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883 },
+ { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074 },
+ { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538 },
+ { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909 },
+ { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786 },
+ { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 },
+ { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 },
+ { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 },
+ { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 },
+ { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 },
+ { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 },
+ { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 },
+ { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 },
+ { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 },
+ { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 },
+ { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 },
+ { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 },
+ { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 },
+ { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 },
+ { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 },
+ { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 },
+ { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 },
+ { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 },
+ { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 },
+ { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 },
+ { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 },
+ { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 },
+ { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 },
+ { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 },
+ { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 },
+ { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 },
+ { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 },
+ { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 },
+ { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 },
+ { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 },
+ { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 },
+ { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982 },
+ { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412 },
+ { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749 },
+ { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527 },
+ { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225 },
+ { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490 },
+ { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525 },
+ { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446 },
+ { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678 },
+ { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200 },
+ { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123 },
+ { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852 },
+ { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484 },
+ { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896 },
+ { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475 },
+ { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013 },
+ { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715 },
+ { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 },
+]
+
+[[package]]
+name = "pyjwt"
+version = "2.10.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 },
+]
+
+[[package]]
+name = "pypinyin"
+version = "0.55.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b4/a4/784cf98c09e0dc22776b0d7d8a4a5b761218bcae4608c2416ce1e167c8af/pypinyin-0.55.0.tar.gz", hash = "sha256:b5711b3a0c6f76e67408ec6b2e3c4987a3a806b7c528076e7c7b86fcf0eaa66b", size = 839836 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b9/7b/4cabc76fcc21c3c7d5c671d8783984d30ac9d3bb387c4ba784fca3cdfa3a/pypinyin-0.55.0-py2.py3-none-any.whl", hash = "sha256:d53b1e8ad2cdb815fb2cb604ed3123372f5a28c6f447571244aca36fc62a286f", size = 840203 },
+]
+
+[[package]]
+name = "pytest"
+version = "8.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474 },
+]
+
+[[package]]
+name = "pytest-asyncio"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976 },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556 },
+]
+
+[[package]]
+name = "python-jose"
+version = "3.5.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ecdsa" },
+ { name = "pyasn1" },
+ { name = "rsa" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c6/77/3a1c9039db7124eb039772b935f2244fbb73fc8ee65b9acf2375da1c07bf/python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b", size = 92726 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624 },
+]
+
+[package.optional-dependencies]
+cryptography = [
+ { name = "cryptography" },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.20"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 },
+]
+
+[[package]]
+name = "pytz"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 },
+ { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 },
+ { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 },
+ { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 },
+ { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 },
+ { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 },
+ { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 },
+ { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 },
+ { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 },
+ { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 },
+ { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 },
+ { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 },
+ { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 },
+ { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 },
+ { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 },
+ { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 },
+ { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 },
+ { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 },
+ { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
+ { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
+ { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
+ { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
+ { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
+ { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
+ { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
+ { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
+ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
+ { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
+ { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
+ { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
+ { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
+ { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
+ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
+ { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
+ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
+]
+
+[[package]]
+name = "regex"
+version = "2024.11.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674 },
+ { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684 },
+ { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589 },
+ { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511 },
+ { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149 },
+ { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707 },
+ { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702 },
+ { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976 },
+ { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397 },
+ { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726 },
+ { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098 },
+ { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325 },
+ { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277 },
+ { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197 },
+ { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714 },
+ { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042 },
+ { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 },
+ { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 },
+ { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 },
+ { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 },
+ { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 },
+ { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 },
+ { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 },
+ { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 },
+ { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 },
+ { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 },
+ { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 },
+ { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 },
+ { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 },
+ { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 },
+ { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 },
+ { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 },
+ { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 },
+ { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 },
+ { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 },
+ { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 },
+ { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 },
+ { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 },
+ { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 },
+ { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 },
+ { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 },
+ { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 },
+ { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 },
+ { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 },
+ { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 },
+ { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 },
+ { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 },
+ { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 },
+ { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 },
+ { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 },
+ { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 },
+ { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 },
+ { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 },
+ { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 },
+ { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 },
+ { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 },
+ { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 },
+ { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 },
+ { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 },
+ { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 },
+ { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 },
+]
+
+[[package]]
+name = "rsa"
+version = "4.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyasn1" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 },
+]
+
+[[package]]
+name = "setuptools"
+version = "80.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
+]
+
+[[package]]
+name = "starlette"
+version = "0.46.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 },
+]
+
+[[package]]
+name = "tenacity"
+version = "9.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248 },
+]
+
+[[package]]
+name = "tiktoken"
+version = "0.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "regex" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ea/cf/756fedf6981e82897f2d570dd25fa597eb3f4459068ae0572d7e888cfd6f/tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d", size = 35991 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/f3/50ec5709fad61641e4411eb1b9ac55b99801d71f1993c29853f256c726c9/tiktoken-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:586c16358138b96ea804c034b8acf3f5d3f0258bd2bc3b0227af4af5d622e382", size = 1065770 },
+ { url = "https://files.pythonhosted.org/packages/d6/f8/5a9560a422cf1755b6e0a9a436e14090eeb878d8ec0f80e0cd3d45b78bf4/tiktoken-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9c59ccc528c6c5dd51820b3474402f69d9a9e1d656226848ad68a8d5b2e5108", size = 1009314 },
+ { url = "https://files.pythonhosted.org/packages/bc/20/3ed4cfff8f809cb902900ae686069e029db74567ee10d017cb254df1d598/tiktoken-0.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0968d5beeafbca2a72c595e8385a1a1f8af58feaebb02b227229b69ca5357fd", size = 1143140 },
+ { url = "https://files.pythonhosted.org/packages/f1/95/cc2c6d79df8f113bdc6c99cdec985a878768120d87d839a34da4bd3ff90a/tiktoken-0.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a5fb085a6a3b7350b8fc838baf493317ca0e17bd95e8642f95fc69ecfed1de", size = 1197860 },
+ { url = "https://files.pythonhosted.org/packages/c7/6c/9c1a4cc51573e8867c9381db1814223c09ebb4716779c7f845d48688b9c8/tiktoken-0.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15a2752dea63d93b0332fb0ddb05dd909371ededa145fe6a3242f46724fa7990", size = 1259661 },
+ { url = "https://files.pythonhosted.org/packages/cd/4c/22eb8e9856a2b1808d0a002d171e534eac03f96dbe1161978d7389a59498/tiktoken-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:26113fec3bd7a352e4b33dbaf1bd8948de2507e30bd95a44e2b1156647bc01b4", size = 894026 },
+ { url = "https://files.pythonhosted.org/packages/4d/ae/4613a59a2a48e761c5161237fc850eb470b4bb93696db89da51b79a871f1/tiktoken-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f32cc56168eac4851109e9b5d327637f15fd662aa30dd79f964b7c39fbadd26e", size = 1065987 },
+ { url = "https://files.pythonhosted.org/packages/3f/86/55d9d1f5b5a7e1164d0f1538a85529b5fcba2b105f92db3622e5d7de6522/tiktoken-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:45556bc41241e5294063508caf901bf92ba52d8ef9222023f83d2483a3055348", size = 1009155 },
+ { url = "https://files.pythonhosted.org/packages/03/58/01fb6240df083b7c1916d1dcb024e2b761213c95d576e9f780dfb5625a76/tiktoken-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03935988a91d6d3216e2ec7c645afbb3d870b37bcb67ada1943ec48678e7ee33", size = 1142898 },
+ { url = "https://files.pythonhosted.org/packages/b1/73/41591c525680cd460a6becf56c9b17468d3711b1df242c53d2c7b2183d16/tiktoken-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b3d80aad8d2c6b9238fc1a5524542087c52b860b10cbf952429ffb714bc1136", size = 1197535 },
+ { url = "https://files.pythonhosted.org/packages/7d/7c/1069f25521c8f01a1a182f362e5c8e0337907fae91b368b7da9c3e39b810/tiktoken-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b2a21133be05dc116b1d0372af051cd2c6aa1d2188250c9b553f9fa49301b336", size = 1259548 },
+ { url = "https://files.pythonhosted.org/packages/6f/07/c67ad1724b8e14e2b4c8cca04b15da158733ac60136879131db05dda7c30/tiktoken-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:11a20e67fdf58b0e2dea7b8654a288e481bb4fc0289d3ad21291f8d0849915fb", size = 893895 },
+ { url = "https://files.pythonhosted.org/packages/cf/e5/21ff33ecfa2101c1bb0f9b6df750553bd873b7fb532ce2cb276ff40b197f/tiktoken-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e88f121c1c22b726649ce67c089b90ddda8b9662545a8aeb03cfef15967ddd03", size = 1065073 },
+ { url = "https://files.pythonhosted.org/packages/8e/03/a95e7b4863ee9ceec1c55983e4cc9558bcfd8f4f80e19c4f8a99642f697d/tiktoken-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6600660f2f72369acb13a57fb3e212434ed38b045fd8cc6cdd74947b4b5d210", size = 1008075 },
+ { url = "https://files.pythonhosted.org/packages/40/10/1305bb02a561595088235a513ec73e50b32e74364fef4de519da69bc8010/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e811743b5dfa74f4b227927ed86cbc57cad4df859cb3b643be797914e41794", size = 1140754 },
+ { url = "https://files.pythonhosted.org/packages/1b/40/da42522018ca496432ffd02793c3a72a739ac04c3794a4914570c9bb2925/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99376e1370d59bcf6935c933cb9ba64adc29033b7e73f5f7569f3aad86552b22", size = 1196678 },
+ { url = "https://files.pythonhosted.org/packages/5c/41/1e59dddaae270ba20187ceb8aa52c75b24ffc09f547233991d5fd822838b/tiktoken-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:badb947c32739fb6ddde173e14885fb3de4d32ab9d8c591cbd013c22b4c31dd2", size = 1259283 },
+ { url = "https://files.pythonhosted.org/packages/5b/64/b16003419a1d7728d0d8c0d56a4c24325e7b10a21a9dd1fc0f7115c02f0a/tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16", size = 894897 },
+ { url = "https://files.pythonhosted.org/packages/7a/11/09d936d37f49f4f494ffe660af44acd2d99eb2429d60a57c71318af214e0/tiktoken-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b0e8e05a26eda1249e824156d537015480af7ae222ccb798e5234ae0285dbdb", size = 1064919 },
+ { url = "https://files.pythonhosted.org/packages/80/0e/f38ba35713edb8d4197ae602e80837d574244ced7fb1b6070b31c29816e0/tiktoken-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:27d457f096f87685195eea0165a1807fae87b97b2161fe8c9b1df5bd74ca6f63", size = 1007877 },
+ { url = "https://files.pythonhosted.org/packages/fe/82/9197f77421e2a01373e27a79dd36efdd99e6b4115746ecc553318ecafbf0/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cf8ded49cddf825390e36dd1ad35cd49589e8161fdcb52aa25f0583e90a3e01", size = 1140095 },
+ { url = "https://files.pythonhosted.org/packages/f2/bb/4513da71cac187383541facd0291c4572b03ec23c561de5811781bbd988f/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc156cb314119a8bb9748257a2eaebd5cc0753b6cb491d26694ed42fc7cb3139", size = 1195649 },
+ { url = "https://files.pythonhosted.org/packages/fa/5c/74e4c137530dd8504e97e3a41729b1103a4ac29036cbfd3250b11fd29451/tiktoken-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd69372e8c9dd761f0ab873112aba55a0e3e506332dd9f7522ca466e817b1b7a", size = 1258465 },
+ { url = "https://files.pythonhosted.org/packages/de/a8/8f499c179ec900783ffe133e9aab10044481679bb9aad78436d239eee716/tiktoken-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ea0edb6f83dc56d794723286215918c1cde03712cbbafa0348b33448faf5b95", size = 894669 },
+]
+
+[[package]]
+name = "tomli"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
+ { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
+ { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
+ { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
+ { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
+ { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
+ { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
+ { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
+ { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
+ { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
+ { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
+ { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
+ { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
+ { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
+ { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
+ { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
+ { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
+ { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
+ { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
+ { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
+ { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
+ { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
+ { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
+ { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
+ { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
+ { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
+ { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
+ { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
+ { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
+ { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
+ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
+]
+
+[[package]]
+name = "tqdm"
+version = "4.67.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.14.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906 },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 },
+]
+
+[[package]]
+name = "tzdata"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.35.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406 },
+]
+
+[[package]]
+name = "virtualenv"
+version = "20.31.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "distlib" },
+ { name = "filelock" },
+ { name = "platformdirs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982 },
+]
+
+[[package]]
+name = "wcwidth"
+version = "0.2.13"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 },
+]
+
+[[package]]
+name = "xlsxwriter"
+version = "3.2.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a7/47/7704bac42ac6fe1710ae099b70e6a1e68ed173ef14792b647808c357da43/xlsxwriter-3.2.5.tar.gz", hash = "sha256:7e88469d607cdc920151c0ab3ce9cf1a83992d4b7bc730c5ffdd1a12115a7dbe", size = 213306 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fa/34/a22e6664211f0c8879521328000bdcae9bf6dbafa94a923e531f6d5b3f73/xlsxwriter-3.2.5-py3-none-any.whl", hash = "sha256:4f4824234e1eaf9d95df9a8fe974585ff91d0f5e3d3f12ace5b71e443c1c6abd", size = 172347 },
+]
+
+[[package]]
+name = "yarl"
+version = "1.20.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "multidict" },
+ { name = "propcache" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/65/7fed0d774abf47487c64be14e9223749468922817b5e8792b8a64792a1bb/yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4", size = 132910 },
+ { url = "https://files.pythonhosted.org/packages/8a/7b/988f55a52da99df9e56dc733b8e4e5a6ae2090081dc2754fc8fd34e60aa0/yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a", size = 90644 },
+ { url = "https://files.pythonhosted.org/packages/f7/de/30d98f03e95d30c7e3cc093759982d038c8833ec2451001d45ef4854edc1/yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed", size = 89322 },
+ { url = "https://files.pythonhosted.org/packages/e0/7a/f2f314f5ebfe9200724b0b748de2186b927acb334cf964fd312eb86fc286/yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e", size = 323786 },
+ { url = "https://files.pythonhosted.org/packages/15/3f/718d26f189db96d993d14b984ce91de52e76309d0fd1d4296f34039856aa/yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73", size = 319627 },
+ { url = "https://files.pythonhosted.org/packages/a5/76/8fcfbf5fa2369157b9898962a4a7d96764b287b085b5b3d9ffae69cdefd1/yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e", size = 339149 },
+ { url = "https://files.pythonhosted.org/packages/3c/95/d7fc301cc4661785967acc04f54a4a42d5124905e27db27bb578aac49b5c/yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8", size = 333327 },
+ { url = "https://files.pythonhosted.org/packages/65/94/e21269718349582eee81efc5c1c08ee71c816bfc1585b77d0ec3f58089eb/yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23", size = 326054 },
+ { url = "https://files.pythonhosted.org/packages/32/ae/8616d1f07853704523519f6131d21f092e567c5af93de7e3e94b38d7f065/yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70", size = 315035 },
+ { url = "https://files.pythonhosted.org/packages/48/aa/0ace06280861ef055855333707db5e49c6e3a08840a7ce62682259d0a6c0/yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb", size = 338962 },
+ { url = "https://files.pythonhosted.org/packages/20/52/1e9d0e6916f45a8fb50e6844f01cb34692455f1acd548606cbda8134cd1e/yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2", size = 335399 },
+ { url = "https://files.pythonhosted.org/packages/f2/65/60452df742952c630e82f394cd409de10610481d9043aa14c61bf846b7b1/yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30", size = 338649 },
+ { url = "https://files.pythonhosted.org/packages/7b/f5/6cd4ff38dcde57a70f23719a838665ee17079640c77087404c3d34da6727/yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309", size = 358563 },
+ { url = "https://files.pythonhosted.org/packages/d1/90/c42eefd79d0d8222cb3227bdd51b640c0c1d0aa33fe4cc86c36eccba77d3/yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24", size = 357609 },
+ { url = "https://files.pythonhosted.org/packages/03/c8/cea6b232cb4617514232e0f8a718153a95b5d82b5290711b201545825532/yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13", size = 350224 },
+ { url = "https://files.pythonhosted.org/packages/ce/a3/eaa0ab9712f1f3d01faf43cf6f1f7210ce4ea4a7e9b28b489a2261ca8db9/yarl-1.20.1-cp310-cp310-win32.whl", hash = "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8", size = 81753 },
+ { url = "https://files.pythonhosted.org/packages/8f/34/e4abde70a9256465fe31c88ed02c3f8502b7b5dead693a4f350a06413f28/yarl-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16", size = 86817 },
+ { url = "https://files.pythonhosted.org/packages/b1/18/893b50efc2350e47a874c5c2d67e55a0ea5df91186b2a6f5ac52eff887cd/yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e", size = 133833 },
+ { url = "https://files.pythonhosted.org/packages/89/ed/b8773448030e6fc47fa797f099ab9eab151a43a25717f9ac043844ad5ea3/yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b", size = 91070 },
+ { url = "https://files.pythonhosted.org/packages/e3/e3/409bd17b1e42619bf69f60e4f031ce1ccb29bd7380117a55529e76933464/yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b", size = 89818 },
+ { url = "https://files.pythonhosted.org/packages/f8/77/64d8431a4d77c856eb2d82aa3de2ad6741365245a29b3a9543cd598ed8c5/yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4", size = 347003 },
+ { url = "https://files.pythonhosted.org/packages/8d/d2/0c7e4def093dcef0bd9fa22d4d24b023788b0a33b8d0088b51aa51e21e99/yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1", size = 336537 },
+ { url = "https://files.pythonhosted.org/packages/f0/f3/fc514f4b2cf02cb59d10cbfe228691d25929ce8f72a38db07d3febc3f706/yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833", size = 362358 },
+ { url = "https://files.pythonhosted.org/packages/ea/6d/a313ac8d8391381ff9006ac05f1d4331cee3b1efaa833a53d12253733255/yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d", size = 357362 },
+ { url = "https://files.pythonhosted.org/packages/00/70/8f78a95d6935a70263d46caa3dd18e1f223cf2f2ff2037baa01a22bc5b22/yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8", size = 348979 },
+ { url = "https://files.pythonhosted.org/packages/cb/05/42773027968968f4f15143553970ee36ead27038d627f457cc44bbbeecf3/yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf", size = 337274 },
+ { url = "https://files.pythonhosted.org/packages/05/be/665634aa196954156741ea591d2f946f1b78ceee8bb8f28488bf28c0dd62/yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e", size = 363294 },
+ { url = "https://files.pythonhosted.org/packages/eb/90/73448401d36fa4e210ece5579895731f190d5119c4b66b43b52182e88cd5/yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389", size = 358169 },
+ { url = "https://files.pythonhosted.org/packages/c3/b0/fce922d46dc1eb43c811f1889f7daa6001b27a4005587e94878570300881/yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f", size = 362776 },
+ { url = "https://files.pythonhosted.org/packages/f1/0d/b172628fce039dae8977fd22caeff3eeebffd52e86060413f5673767c427/yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845", size = 381341 },
+ { url = "https://files.pythonhosted.org/packages/6b/9b/5b886d7671f4580209e855974fe1cecec409aa4a89ea58b8f0560dc529b1/yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1", size = 379988 },
+ { url = "https://files.pythonhosted.org/packages/73/be/75ef5fd0fcd8f083a5d13f78fd3f009528132a1f2a1d7c925c39fa20aa79/yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e", size = 371113 },
+ { url = "https://files.pythonhosted.org/packages/50/4f/62faab3b479dfdcb741fe9e3f0323e2a7d5cd1ab2edc73221d57ad4834b2/yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773", size = 81485 },
+ { url = "https://files.pythonhosted.org/packages/f0/09/d9c7942f8f05c32ec72cd5c8e041c8b29b5807328b68b4801ff2511d4d5e/yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e", size = 86686 },
+ { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667 },
+ { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025 },
+ { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709 },
+ { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287 },
+ { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429 },
+ { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429 },
+ { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862 },
+ { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616 },
+ { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954 },
+ { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575 },
+ { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061 },
+ { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142 },
+ { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894 },
+ { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378 },
+ { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069 },
+ { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249 },
+ { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710 },
+ { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811 },
+ { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078 },
+ { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748 },
+ { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595 },
+ { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616 },
+ { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324 },
+ { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676 },
+ { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614 },
+ { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766 },
+ { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615 },
+ { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982 },
+ { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792 },
+ { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049 },
+ { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774 },
+ { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252 },
+ { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198 },
+ { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346 },
+ { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826 },
+ { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217 },
+ { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700 },
+ { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644 },
+ { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452 },
+ { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378 },
+ { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261 },
+ { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987 },
+ { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361 },
+ { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460 },
+ { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486 },
+ { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219 },
+ { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693 },
+ { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803 },
+ { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709 },
+ { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591 },
+ { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003 },
+ { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542 },
+]