diff --git a/.cursor/rules/academic-catalog-enrichment.mdc b/.cursor/rules/academic-catalog-enrichment.mdc index 7bca0a5..1c6560d 100644 --- a/.cursor/rules/academic-catalog-enrichment.mdc +++ b/.cursor/rules/academic-catalog-enrichment.mdc @@ -16,7 +16,7 @@ The Academic Catalog Enrichment system provides integration with academic reposi │ ▼ ┌─────────────────────────────────────────────────────────────────┐ -│ AcademicCatalogEnrichmentResult │ +│ linkedEntitiesEnrichmentResult │ │ ┌────────────────┬──────────────────┬──────────────────────┐ │ │ │ repository_ │ author_relations │ organization_ │ │ │ │ relations │ Dict[str, List] │ relations │ │ @@ -36,13 +36,13 @@ The Academic Catalog Enrichment system provides integration with academic reposi ## Data Models ### Location -- **Path**: `src/data_models/academic_catalog.py` +- **Path**: `src/data_models/linked_entities.py` ### Key Models -#### 1. AcademicCatalogRelation +#### 1. linkedEntitiesRelation ```python -class AcademicCatalogRelation(BaseModel): +class linkedEntitiesRelation(BaseModel): """A single relation to an academic catalog entity.""" catalogType: CatalogType # "infoscience", "openalex", "epfl_graph" @@ -54,19 +54,19 @@ class AcademicCatalogRelation(BaseModel): # Note: externalId field has been removed ``` -#### 2. AcademicCatalogEnrichmentResult (Structured Output) +#### 2. linkedEntitiesEnrichmentResult (Structured Output) ```python -class AcademicCatalogEnrichmentResult(BaseModel): +class linkedEntitiesEnrichmentResult(BaseModel): """Organized results by what was searched for.""" # Publications about the repository/project itself - repository_relations: List[AcademicCatalogRelation] = [] + repository_relations: List[linkedEntitiesRelation] = [] # Keyed by exact author name provided - author_relations: Dict[str, List[AcademicCatalogRelation]] = {} + author_relations: Dict[str, List[linkedEntitiesRelation]] = {} # Keyed by exact organization name provided - organization_relations: Dict[str, List[AcademicCatalogRelation]] = {} + organization_relations: Dict[str, List[linkedEntitiesRelation]] = {} # Metadata searchStrategy: Optional[str] = None @@ -102,7 +102,7 @@ result = agent.run(prompt, authors=["Alexander Mathis", ...]) } } # Direct assignment: -author.academicCatalogRelations = result.author_relations[author.name] +author.linkedEntities = result.author_relations[author.name] ``` ### Agent Responsibilities @@ -124,8 +124,8 @@ Python code is responsible for: ### 1. Agent Call ```python -# src/agents/academic_catalog_enrichment.py -async def enrich_repository_academic_catalog( +# src/agents/linked_entities_enrichment.py +async def enrich_repository_linked_entities( repository_url: str, repository_name: str, description: str, @@ -161,9 +161,9 @@ async def enrich_repository_academic_catalog( ### 3. Direct Assignment ```python # src/analysis/repositories.py -async def run_academic_catalog_enrichment(self): +async def run_linked_entities_enrichment(self): # Call agent - result = await enrich_repository_academic_catalog( + result = await enrich_repository_linked_entities( repository_url=self.full_path, repository_name=repository_name, authors=author_names, # ["Alexander Mathis", ...] @@ -173,21 +173,21 @@ async def run_academic_catalog_enrichment(self): enrichment_data = result.get("data") # 1. Repository-level - self.data.academicCatalogRelations = enrichment_data.repository_relations + self.data.linkedEntities = enrichment_data.repository_relations # 2. Author-level (direct lookup by name) for author in self.data.author: if author.name in enrichment_data.author_relations: - author.academicCatalogRelations = enrichment_data.author_relations[author.name] + author.linkedEntities = enrichment_data.author_relations[author.name] else: - author.academicCatalogRelations = [] + author.linkedEntities = [] # 3. Organization-level (direct lookup by name) for org in self.data.author: if org.legalName in enrichment_data.organization_relations: - org.academicCatalogRelations = enrichment_data.organization_relations[org.legalName] + org.linkedEntities = enrichment_data.organization_relations[org.legalName] else: - org.academicCatalogRelations = [] + org.linkedEntities = [] ``` **Key Points**: @@ -309,10 +309,10 @@ return InfoscienceAuthor( 4. **Agent extracts from markdown**: - Agent prompt explicitly instructs: "Extract UUID from '*UUID:* ' in markdown" - - Agent populates `AcademicCatalogRelation.uuid` field + - Agent populates `linkedEntitiesRelation.uuid` field - Agent populates `entity.uuid` in the full entity object -**Chain of custody**: API → Parser → Pydantic Model → Markdown → Agent → AcademicCatalogRelation +**Chain of custody**: API → Parser → Pydantic Model → Markdown → Agent → linkedEntitiesRelation #### 3. Markdown as Transport Layer Since tools return markdown (not structured data), markdown must include ALL critical fields: @@ -466,8 +466,10 @@ ENV_VAR_MAPPINGS = { | LLM Analysis | `run_llm_analysis` | o4-mini | Main repository analysis | | User Enrichment | `run_user_enrichment` | o4-mini | Author enrichment with ORCID | | Org Enrichment | `run_organization_enrichment` | o4-mini | ROR matching | -| Academic Catalog | `run_academic_catalog_enrichment` | o4-mini | Infoscience searches (tool-heavy) | +| Academic Catalog | `run_linked_entities_searcher` | o4-mini | Infoscience searches (tool-heavy, repository-level only) | | EPFL Assessment | `run_epfl_assessment` | o4-mini | Final holistic assessment | +| Repository Classifier | `run_repository_classifier` | o4-mini | Repository type and discipline classification | +| Organization Identifier | `run_organization_identifier` | o4-mini | Organization identification | ## Integration into Analysis Pipeline @@ -475,33 +477,58 @@ ENV_VAR_MAPPINGS = { ```python # src/analysis/repositories.py -async def run_analysis(self): +async def run_analysis(self, run_author_linked_entities: bool = False): # 1. Extract metadata with GIMIE await self.run_gimie() - # 2. LLM analysis (main agent) - await self.run_llm_analysis() + # 2. Atomic LLM pipeline (stages 1-5) + await self.run_atomic_llm_pipeline() + # Stage 1: Context compiler + # Stage 2: Structured output + # Stage 3: Repository classifier + # Stage 4: Organization identifier + # Stage 5: Linked entities searcher (repository-level only) # 3. ORCID enrichment (no LLM) self.run_authors_enrichment() - # 4. Organization enrichment (ROR agent) + # 4. User enrichment (optional) + await self.run_user_enrichment() + + # 5. Organization enrichment (optional) await self.run_organization_enrichment() - # 5. User enrichment (author agent) - await self.run_user_enrichment() + # 6. Academic catalog enrichment (repository-level - runs in atomic pipeline) + # Already completed in Stage 5 of atomic pipeline - # 6. Academic catalog enrichment (NEW!) - await self.run_academic_catalog_enrichment() + # 7. Optional: Author-level linked entities enrichment + if run_author_linked_entities: + await self.run_author_linked_entities_enrichment() - # 7. Final EPFL assessment (holistic) + # 8. Final EPFL assessment (holistic) await self.run_epfl_final_assessment() ``` **Order matters**: -- Academic catalog enrichment runs AFTER user/org enrichment (needs author names) +- Academic catalog enrichment (repository-level) runs in Stage 5 of atomic pipeline +- Author-level linked entities enrichment is optional and runs separately - EPFL assessment runs LAST (reviews all collected data) +### Linked Entities Enrichment Scope + +**Repository-Level (Default)**: +- Runs automatically in Stage 5 of atomic pipeline +- Searches Infoscience for publications about the repository/tool name +- Stores results in `repository.linkedEntities` +- Uses `search_infoscience_publications_tool` with repository name as query + +**Author-Level (Optional)**: +- Controlled by `run_author_linked_entities` parameter +- Separate method: `run_author_linked_entities_enrichment()` +- Searches Infoscience for each author individually +- Assigns results to `author.linkedEntities` for each Person +- Only runs when explicitly requested (default: `False`) + ### Estimated Token Accumulation **EVERY agent must accumulate estimated tokens**: @@ -517,7 +544,7 @@ if usage and "estimated_input_tokens" in usage: - ✅ `run_llm_analysis()` - ✅ `run_organization_enrichment()` - ✅ `run_user_enrichment()` -- ✅ `run_academic_catalog_enrichment()` +- ✅ `run_linked_entities_enrichment()` - ✅ `run_epfl_final_assessment()` ## Testing Guidelines @@ -539,8 +566,8 @@ curl "http://0.0.0.0:1234/v1/extract/json/https://github.com/DeepLabCut/DeepLabC ### Verification Checklist -- [ ] Repository `academicCatalogRelations` populated -- [ ] Each author has `academicCatalogRelations` (may be empty) +- [ ] Repository `linkedEntities` populated +- [ ] Each author has `linkedEntities` (may be empty) - [ ] Relations include full entity objects (not just UUIDs) - [ ] **UUIDs are populated** (not null) for all matched entities - [ ] **URLs/profile_urls are populated** for all matched entities @@ -552,7 +579,7 @@ curl "http://0.0.0.0:1234/v1/extract/json/https://github.com/DeepLabCut/DeepLabC ## Common Issues & Solutions -### Issue: UUID is null in academicCatalogRelations +### Issue: UUID is null in linkedEntities **Cause**: Field name mismatch in parser (e.g., `url=` instead of `profile_url=`) **Symptoms**: ```json @@ -604,6 +631,26 @@ curl "http://0.0.0.0:1234/v1/extract/json/https://github.com/DeepLabCut/DeepLabC **Cause**: Parser passing wrong field name to Pydantic model **Solution**: Pydantic silently ignores unknown fields - verify field names match model definition +### Issue: Validation errors for union fields (entityInfosciencePublication, entityInfoscienceAuthor, entityInfoscienceLab) +**Cause**: LLM populating all three union fields with the same data, or wrong entity type in wrong field +**Symptoms**: +```json +{ + "entityType": "publication", + "entityInfosciencePublication": {...}, // ✅ Correct + "entityInfoscienceAuthor": {...}, // ❌ Should be None/omitted + "entityInfoscienceLab": {...} // ❌ Should be None/omitted +} +``` + +**Solution**: +1. **System prompt**: Explicitly instruct LLM to populate ONLY the field matching `entityType` +2. **Reconciliation method**: `_reconcile_entity_union()` in `repositories.py`: + - Checks `entityType` to select correct union variant + - Removes other two fields + - Converts `None` to empty lists for list fields (`subjects`, `authors`, `keywords`) +3. **List field handling**: Convert `None` to `[]` for list fields before validation + ## Future Extensions ### Adding New Catalogs @@ -642,7 +689,7 @@ Future enhancement: Match same entities across catalogs using: ```python # Example future feature -def deduplicate_across_catalogs(relations: List[AcademicCatalogRelation]): +def deduplicate_across_catalogs(relations: List[linkedEntitiesRelation]): """Merge same entities from different catalogs.""" # Group by DOI, ORCID, or other stable identifiers # Provide unified view across catalogs @@ -660,8 +707,8 @@ def deduplicate_across_catalogs(relations: List[AcademicCatalogRelation]): ## References -- Implementation: `src/agents/academic_catalog_enrichment.py` -- Data Models: `src/data_models/academic_catalog.py` +- Implementation: `src/agents/linked_entities_enrichment.py` +- Data Models: `src/data_models/linked_entities.py` - Infoscience Client: `src/context/infoscience.py` - Integration: `src/analysis/repositories.py` -- Documentation: `ACADEMIC_CATALOG_OPTION_B_IMPLEMENTATION.md` (if exists) +- Documentation: `linked_entities_OPTION_B_IMPLEMENTATION.md` (if exists) diff --git a/.cursor/rules/ai-agents.mdc b/.cursor/rules/ai-agents.mdc index e8a0044..226416a 100644 --- a/.cursor/rules/ai-agents.mdc +++ b/.cursor/rules/ai-agents.mdc @@ -18,12 +18,17 @@ alwaysApply: true - Environment variable overrides supported #### Model Types -Five analysis types with separate configs: +Multiple analysis types with separate configs: - `run_llm_analysis` - Repository analysis - `run_user_enrichment` - User metadata enrichment - `run_organization_enrichment` - Organization metadata enrichment -- `run_academic_catalog_enrichment` - Academic catalog searches (Infoscience, etc.) +- `run_linked_entities_enrichment` - Academic catalog searches (Infoscience, etc.) - `run_epfl_assessment` - Final EPFL relationship assessment +- `run_repository_classifier` - Repository type and discipline classification (atomic agent) +- `run_organization_identifier` - Organization identification (atomic agent) +- `run_structured_output` - Structured metadata extraction (atomic agent) +- `run_context_compiler` - Repository context compilation (atomic agent) +- `run_linked_entities_searcher` - Academic catalog search with tools (atomic agent) #### Provider Support - **OpenAI**: Direct OpenAI API @@ -60,6 +65,13 @@ model = create_pydantic_ai_model(configs[0]) - **Enrichment Agents:** - User enrichment: `src/agents/user_enrichment.py` (uses `UserEnrichmentResult` schema) - Organization enrichment: `src/agents/organization_enrichment.py` (uses `OrganizationEnrichmentResult` schema) + - **ORCID enrichment**: `src/utils/utils.py` (functions: `enrich_authors_with_orcid`, `enrich_author_with_orcid`) +- **Atomic Agents** (multi-stage pipeline): + - Context compiler: `src/agents/atomic_agents/context_compiler.py` (uses `CompiledContext` schema) + - Structured output: `src/agents/atomic_agents/structured_output.py` (uses dynamically generated simplified model) + - Repository classifier: `src/agents/atomic_agents/repository_classifier.py` (uses `RepositoryClassification` schema) + - Organization identifier: `src/agents/atomic_agents/organization_identifier.py` (uses `OrganizationIdentification` schema) + - Linked entities searcher: `src/agents/atomic_agents/linked_entities_searcher.py` (uses `LinkedEntitiesContext` and `linkedEntitiesEnrichmentResult` schemas) - **Final Assessment Agent:** - EPFL assessment: `src/agents/epfl_assessment.py` (uses `EPFLAssessmentResult` schema) @@ -291,6 +303,179 @@ result = await run_agent_with_fallback( - **User Enrichment**: `UserEnrichmentResult` - **Organization Enrichment**: `OrganizationEnrichmentResult` - **EPFL Assessment**: `EPFLAssessmentResult` (from `data_models/epfl_assessment.py`) +- **Atomic Agents**: + - Context Compiler: `CompiledContext` (from `atomic_agents/models.py`) + - Structured Output: Dynamically generated simplified model (from `SoftwareSourceCode`) + - Repository Classifier: `RepositoryClassification` (from `atomic_agents/models.py`) - uses `Literal` types for `discipline` and `repositoryType` with enum constraints + - Organization Identifier: `OrganizationIdentification` (from `atomic_agents/models.py`) - uses `SimplifiedOrganization` with `name` (not `legalName`) for LLM compatibility + - Linked Entities Searcher: `LinkedEntitiesContext` and `linkedEntitiesEnrichmentResult` (from `data_models/linked_entities.py`) + +## ORCID Enrichment Integration + +### Overview +ORCID enrichment automatically fetches affiliation data from ORCID API for authors who have ORCID identifiers. This runs after the atomic LLM pipeline completes and before optional user/organization enrichments. + +**Location**: `src/utils/utils.py` + +### Key Functions + +#### `enrich_authors_with_orcid(repositoryObject: SoftwareSourceCode)` +Enriches all Person authors in a repository who have ORCID IDs: +- Skips Organization objects (only enriches Person) +- Skips authors without ORCID IDs +- Fetches employment history from ORCID API +- Creates `Affiliation` objects with `source="orcid"` +- Merges with existing affiliations (no duplicates, case-insensitive) + +#### `enrich_author_with_orcid(author: Person)` +Enriches a single Person with ORCID affiliations: +- Validates and normalizes ORCID ID to URL format +- Calls `get_orcid_affiliations()` to fetch data +- Returns enriched Person object + +#### `get_orcid_affiliations(orcid_id: str) -> List[Affiliation]` +Fetches affiliations from ORCID API: +- Returns list of `Affiliation` objects with `source="orcid"` +- Cleans organization names (removes location suffixes) +- Uses ORCID web scraping via Selenium +- Cached to prevent duplicate API calls + +### Integration in Analysis Pipeline + +**File**: `src/analysis/repositories.py` + +ORCID enrichment runs automatically in `run_analysis()`: + +```python +async def run_analysis( + self, + run_gimie: bool = True, + run_llm: bool = True, + run_user_enrichment: bool = True, + run_organization_enrichment: bool = True, +): + # 1. GIMIE analysis + if run_gimie: + self.run_gimie_analysis() + + # 2. Atomic LLM pipeline (stages 1-3) + if run_llm: + await self.run_llm_analysis() + + # 3. ORCID enrichment (NEW - runs automatically after LLM) + if self.data is not None: + logging.info(f"ORCID enrichment for {self.full_path}") + self.run_authors_enrichment() + logging.info(f"ORCID enrichment completed for {self.full_path}") + + # 4. User enrichment (optional) + if run_user_enrichment and self.data is not None: + await self.run_user_enrichment() + + # 5. Organization enrichment (optional) + if run_organization_enrichment and self.data is not None: + await self.run_organization_enrichment() +``` + +### Execution Order +1. Cache check +2. GIMIE analysis (if `run_gimie=True`) +3. Atomic LLM pipeline (if `run_llm=True`) +4. **ORCID enrichment** (always runs if `self.data` exists after LLM) +5. User enrichment (if `run_user_enrichment=True`) +6. Organization enrichment (if `run_organization_enrichment=True`) +7. Validation and caching + +### Benefits +- ✅ **Automatic enrichment** for authors with ORCID IDs +- ✅ **Provenance tracking** via `source="orcid"` in Affiliation objects +- ✅ **No duplicates** - merges with existing affiliations intelligently +- ✅ **No LLM tokens** consumed - uses ORCID API directly +- ✅ **Runs before other enrichments** so they can utilize ORCID data + +### Example +```python +# Before ORCID enrichment +person = Person( + name="John Doe", + orcid="0000-0002-1234-5678", + affiliations=[], +) + +# After ORCID enrichment +person = Person( + name="John Doe", + orcid="https://orcid.org/0000-0002-1234-5678", # Normalized to URL + affiliations=[ + Affiliation( + name="EPFL - École Polytechnique Fédérale de Lausanne", + organizationId=None, + source="orcid", + ), + Affiliation( + name="Swiss Data Science Center", + organizationId=None, + source="orcid", + ), + ], +) +``` + +## Agent Configuration + +### Agent Creation with Retries + +**File**: `src/agents/agents_management.py` + +Agents are created with retry configuration for validation errors: + +```python +agent = Agent( + model=model, + output_type=output_type, + system_prompt=system_prompt, + tools=agent_tools, + retries=3, # Allow model to retry up to 3 times on validation errors +) +``` + +**Parameter**: `retries` (not `result_retries` or `output_retries`) +- Controls retry attempts for tool calls and output validation +- Default: 1 (only one attempt) +- Recommended: 3 (gives LLM multiple chances to correct validation errors) + +### Validation Error Handling + +When LLMs produce output that doesn't match the schema: +1. PydanticAI validates output against `output_type` schema +2. If validation fails, LLM gets error message and can retry +3. Up to `retries` attempts before raising `UnexpectedModelBehavior` +4. Each retry includes the validation error details for correction + +### Enhanced Error Logging + +Detailed exception chain logging for debugging: + +```python +if "validation" in error_msg.lower() or "retries" in error_msg.lower(): + logger.error(f"Agent run failed with validation error: {e}", exc_info=True) + + # Traverse nested exception chains + if hasattr(e, "__cause__") and e.__cause__: + logger.error(f"Underlying cause: {e.__cause__}") + cause = e.__cause__ + depth = 0 + while hasattr(cause, "__cause__") and cause.__cause__ and depth < 5: + cause = cause.__cause__ + logger.error(f"Nested cause (depth {depth + 1}): {cause}") + depth += 1 +``` + +Benefits: +- See exact validation errors (field names, types, expected vs actual) +- Identify which fields are causing issues +- Debug LLM output format problems +- Trace through pydantic_core.ValidationError chains ## Analysis Context @@ -770,17 +955,17 @@ For agents that search for multiple entities (e.g., academic catalog enrichment) #### Pattern: Organized Results ```python -class AcademicCatalogEnrichmentResult(BaseModel): +class linkedEntitiesEnrichmentResult(BaseModel): """Results organized by what was searched for.""" # Repository-level results - repository_relations: List[AcademicCatalogRelation] = [] + repository_relations: List[linkedEntitiesRelation] = [] # Author-level results (keyed by exact name provided) - author_relations: Dict[str, List[AcademicCatalogRelation]] = {} + author_relations: Dict[str, List[linkedEntitiesRelation]] = {} # Organization-level results (keyed by exact name provided) - organization_relations: Dict[str, List[AcademicCatalogRelation]] = {} + organization_relations: Dict[str, List[linkedEntitiesRelation]] = {} ``` #### Agent Responsibilities @@ -798,7 +983,7 @@ class AcademicCatalogEnrichmentResult(BaseModel): #### Example Implementation ```python # Call agent with exact names -result = await enrich_repository_academic_catalog( +result = await enrich_repository_linked_entities( repository_name="DeepLabCut", authors=["Alexander Mathis", "Mackenzie Weygandt Mathis"], organizations=["DeepLabCut"], @@ -808,14 +993,14 @@ result = await enrich_repository_academic_catalog( enrichment_data = result.get("data") # Repository level -self.data.academicCatalogRelations = enrichment_data.repository_relations +self.data.linkedEntities = enrichment_data.repository_relations # Author level (direct lookup) for author in self.data.author: if author.name in enrichment_data.author_relations: - author.academicCatalogRelations = enrichment_data.author_relations[author.name] + author.linkedEntities = enrichment_data.author_relations[author.name] else: - author.academicCatalogRelations = [] + author.linkedEntities = [] ``` **Benefits**: @@ -829,6 +1014,146 @@ for author in self.data.author: - Need to assign results back to specific source objects - Name variations exist in the target system +## Atomic Agents Pipeline + +### Overview +The atomic agents pipeline is a multi-stage LLM-based system for repository metadata extraction. It breaks down the complex task of repository analysis into smaller, focused stages that can be optimized independently. + +### Pipeline Stages + +**Location**: `src/agents/atomic_agents/` + +#### Stage 1: Context Compiler +- **File**: `context_compiler.py` +- **Purpose**: Compiles repository information (GIMIE data, git authors, README) into structured markdown +- **Output**: `CompiledContext` with markdown content and repository metadata +- **Tools**: None (pure compilation) + +#### Stage 2: Structured Output +- **File**: `structured_output.py` +- **Purpose**: Extracts structured metadata from compiled context +- **Output**: Simplified repository model with core fields (description, applicationCategory, authors, etc.) +- **Tools**: None (structured extraction) +- **Note**: Does NOT extract `discipline`, `repositoryType`, or `relatedToOrganizations` (handled by later stages) + +#### Stage 3: Repository Classifier +- **File**: `repository_classifier.py` +- **Purpose**: Classifies repository type and scientific disciplines +- **Output**: `RepositoryClassification` with: + - `repositoryType`: Required, from `RepositoryType` enum (software, educational resource, documentation, data, webpage, other) + - `discipline`: Required list, from `Discipline` enum (at least one discipline required) + - Justifications for each classification +- **Schema Enforcement**: Uses `Literal` types derived from enums to enforce valid values +- **Tools**: None (classification only) + +#### Stage 4: Organization Identifier +- **File**: `organization_identifier.py` +- **Purpose**: Identifies institutional organizations directly related to the software +- **Output**: `OrganizationIdentification` with: + - `relatedToOrganizations`: List of `SimplifiedOrganization` objects + - `relatedToOrganizationJustification`: List of justifications +- **Key Requirements**: + - Focus on **direct institutional relationships** (developers, maintainers, sponsors, hosts) + - **NOT** just author affiliations - organization must be directly involved with the software + - `organizationType` is **required** for each organization + - `attributionConfidence` is optional but recommended +- **Simplified Model**: Uses `name` (not `legalName`) for LLM compatibility, converted to `legalName` during reconciliation +- **Tools**: None (identification only) + +#### Stage 5: Linked Entities Searcher (Academic Catalog) +- **File**: `linked_entities_searcher.py` +- **Purpose**: Searches academic catalogs (Infoscience) for publications related to the repository +- **Output**: `linkedEntitiesEnrichmentResult` with `repository_relations` only +- **Tools**: `search_infoscience_publications_tool` +- **Scope**: Repository-level searches only (searches for publications about the repository/tool name) +- **Author-level searches**: Handled separately in optional `run_author_linked_entities_enrichment()` method + +### Pipeline Execution Order + +```python +# In src/analysis/repositories.py - run_atomic_llm_pipeline() +# Stage 1: Compile context +compiled_context = await compile_repository_context(...) + +# Stage 2: Generate structured output +structured_output = await generate_structured_output(compiled_context, ...) + +# Stage 3: Classify repository (overrides Stage 2 values) +classification = await classify_repository_type_and_discipline(compiled_context) +# Override: repositoryType, discipline, justifications + +# Stage 4: Identify organizations +organizations = await identify_related_organizations(compiled_context) +# Set: relatedToOrganizations, relatedToOrganizationJustification + +# Stage 5: Search academic catalogs (repository-level only) +linked_entities = await search_academic_catalogs(repository_name) +# Set: repository.linkedEntities +``` + +### Key Design Principles + +1. **Stage Independence**: Each stage can be optimized independently +2. **Schema Enforcement**: All stages use Pydantic models with strict validation +3. **Field Override Pattern**: Later stages can override earlier stage values (e.g., Stage 3 overrides Stage 2's classification) +4. **Simplified Models**: Use intuitive field names for LLMs (`name` not `legalName`), convert during reconciliation +5. **Union Field Reconciliation**: Handle split union fields (e.g., `entityInfosciencePublication`, `entityInfoscienceAuthor`, `entityInfoscienceLab`) based on `entityType` + +### Repository Classification Schema + +**Critical**: The `RepositoryClassification` model uses `Literal` types to enforce enum constraints: + +```python +# In src/agents/atomic_agents/models.py +from ...data_models.models import Discipline, RepositoryType + +# Extract values from enums (avoid duplication) +ValidDiscipline = Literal["Social sciences", "Anthropology", ...] # All Discipline enum values +ValidRepositoryType = Literal["software", "educational resource", ...] # All RepositoryType enum values + +# Runtime verification ensures Literal values match enum values +assert set(get_args(ValidDiscipline)) == {d.value for d in Discipline} +assert set(get_args(ValidRepositoryType)) == {rt.value for rt in RepositoryType} + +class RepositoryClassification(BaseModel): + repositoryType: ValidRepositoryType # Required, enforced by enum + discipline: List[ValidDiscipline] # Required, at least one, enforced by enum + # ... justifications +``` + +**Benefits**: +- LLM receives JSON schema with `enum` constraints +- Pydantic validates against `Literal` types +- Runtime assertions ensure synchronization with source enums +- Fail-fast if values drift out of sync + +### Organization Identifier Requirements + +**System Prompt Emphasis**: +- **Direct relationship required**: Organization must be directly involved with software (development, funding, hosting, partnership) +- **NOT sufficient**: Side affiliation of an author (unless organization is directly involved) +- **Required fields**: `organizationType` (required), `attributionConfidence` (optional) +- **Field naming**: Use `name` in simplified model, convert to `legalName` in full model + +### Linked Entities Enrichment + +**Repository-Level Only (Default)**: +- Searches Infoscience for publications about the repository/tool name +- Stores results in `repository.linkedEntities` +- Runs automatically in main pipeline + +**Author-Level (Optional)**: +- Separate method: `run_author_linked_entities_enrichment()` +- Controlled by `run_author_linked_entities` parameter in `run_analysis()` +- Searches Infoscience for each author individually +- Assigns results to `author.linkedEntities` for each Person + +**Union Field Handling**: +- Entity union fields are split into three fields: `entityInfosciencePublication`, `entityInfoscienceAuthor`, `entityInfoscienceLab` +- Reconciliation method (`_reconcile_entity_union`) selects correct field based on `entityType` +- Converts `None` to empty lists for list fields (`subjects`, `authors`, `keywords`) +- Only one union variant should be populated per relation + ### Best Practices 1. **Always extract usage**: Check both direct attributes and `details` field 2. **Provide estimates**: Use `estimate_tokens_from_messages()` for client-side token counting as fallback @@ -839,7 +1164,7 @@ for author in self.data.author: - ✅ `run_llm_analysis()` - Repository analysis - ✅ `run_organization_enrichment()` - ROR matching - ✅ `run_user_enrichment()` - Author enrichment - - ✅ `run_academic_catalog_enrichment()` - Infoscience searches + - ✅ `run_linked_entities_enrichment()` - Infoscience searches - ✅ `run_epfl_final_assessment()` - Final assessment - Each must accumulate both `self.total_*_tokens` AND `self.estimated_*_tokens` 5. **Track timing**: Record start/end times in `run_analysis()`, calculate duration in `get_usage_stats()` @@ -853,7 +1178,7 @@ for author in self.data.author: - Pre-searched ROR data: Only essential fields (name, ror_id, country, website, 3 aliases, parent orgs) - Prompt instructions: Concise and focused, avoid verbose explanations - Token breakdown logging: Helps identify which sections consume most tokens -13. **Token accumulation verification**: +13. **Token accumulation verification**: - All agents accumulate tokens in `Repository` class instance variables - `get_usage_stats()` returns accumulated totals from all agents - `APIStats.calculate_total_tokens()` calculates both official and estimated totals diff --git a/.cursor/rules/deployment-and-running.mdc b/.cursor/rules/deployment-and-running.mdc index dd1865b..4b09434 100644 --- a/.cursor/rules/deployment-and-running.mdc +++ b/.cursor/rules/deployment-and-running.mdc @@ -313,7 +313,7 @@ USER_ENRICHMENT_MODELS='[...]' ORG_ENRICHMENT_MODELS='[...]' # Academic catalog enrichment models (NEW) -ACADEMIC_CATALOG_ENRICHMENT_MODELS='[ +linked_entities_ENRICHMENT_MODELS='[ { "provider": "openai", "model": "o4-mini", diff --git a/.cursor/rules/project-architecture.mdc b/.cursor/rules/project-architecture.mdc index 3d52a8e..a08fcc3 100644 --- a/.cursor/rules/project-architecture.mdc +++ b/.cursor/rules/project-architecture.mdc @@ -64,23 +64,26 @@ src/ - Type discriminator: `type: Literal["Person"]` (auto-defaults) - Identity: `name`, `email` (Union[str, List[str]]), `orcid` (string), `gitAuthorIds` - Affiliations: `affiliations`, `currentAffiliation`, `affiliationHistory` -- Metadata: `contributionSummary`, `biography`, `academicCatalogRelations` +- Provenance: `source` (gimie, llm, orcid, agent_user_enrichment, github_profile) +- Metadata: `contributionSummary`, `biography`, `linkedEntities` - Backward compatibility: `affiliation` field deprecated but kept -**Organization Model** - Institutions and companies: +**Organization Model** - Institutions and companies (simplified fields): - Type discriminator: `type: Literal["Organization"]` (auto-defaults) -- Identity: `legalName`, `hasRorId`, `alternateNames` -- Details: `organizationType`, `parentOrganization`, `country`, `website` -- Metadata: `attributionConfidence`, `academicCatalogRelations` - -#### Academic Catalog Models (`academic_catalog.py`) -**AcademicCatalogRelation** - Relation to an academic catalog entity: +- Identity: `legalName`, `hasRorId` +- Details: `organizationType` +- Provenance: `source` (gimie, llm, agent_org_enrichment, github_profile) +- Metadata: `attributionConfidence`, `linkedEntities` +- Removed fields: ❌ `alternateNames`, `parentOrganization`, `country`, `website` + +#### Academic Catalog Models (`linked_entities.py`) +**linkedEntitiesRelation** - Relation to an academic catalog entity: - Catalog: `catalogType` (infoscience, openalex, epfl_graph) - Entity: `entityType` (publication, person, orgunit), full `entity` object - Matching: `confidence`, `justification`, `matchedOn` fields -- Identifiers: `uuid`, `url` (Note: `externalId` removed from AcademicCatalogRelation) +- Identifiers: `uuid`, `url` (Note: `externalId` removed from linkedEntitiesRelation) -**AcademicCatalogEnrichmentResult** - Structured agent output: +**linkedEntitiesEnrichmentResult** - Structured agent output: - `repository_relations`: Publications about the repository itself - `author_relations`: Dict[author_name, List[relations]] for each author - `organization_relations`: Dict[org_name, List[relations]] for each org @@ -109,7 +112,7 @@ src/ - `repository_enrichment.py` - Main repository analysis - `user_enrichment.py` - Author/person enrichment with ORCID - `organization_enrichment.py` - ROR matching for organizations - - `academic_catalog_enrichment.py` - Infoscience/academic catalog searches + - `linked_entities_enrichment.py` - Infoscience/academic catalog searches - `epfl_assessment.py` - Final holistic EPFL relationship assessment - **Structured Output Pattern**: Agents return organized dicts keyed by input names for direct assignment diff --git a/.cursor/rules/pydantic-models.mdc b/.cursor/rules/pydantic-models.mdc index d7179f9..5649c56 100644 --- a/.cursor/rules/pydantic-models.mdc +++ b/.cursor/rules/pydantic-models.mdc @@ -48,9 +48,9 @@ class Person(BaseModel): # Core identity fields name: str = Field(description="Person's name") - email: Optional[Union[str, List[str]]] = Field( - description="Email address(es) - can be a single string or a list of strings", - default=None, + emails: Optional[List[str]] = Field( + description="Email address(es) - list of strings (automatically anonymized after validation)", + default_factory=list, ) orcid: Optional[str] = Field( description="ORCID identifier (format: 0000-0000-0000-0000 or https://orcid.org/0000-0000-0000-0000). Examples: '0000-0002-1234-5678', '0000-0000-0000-000X'", @@ -62,23 +62,25 @@ class Person(BaseModel): ) # Affiliation fields - affiliation: Optional[List[str]] = Field( - description="List of affiliations (deprecated, use affiliations)", - default=None, - ) - affiliations: List[str] = Field( - description="List of all identified affiliations", + affiliations: List[Affiliation] = Field( + description="List of current affiliations with provenance tracking", default_factory=list, ) currentAffiliation: Optional[str] = Field( description="Most recent or current affiliation", default=None, ) - affiliationHistory: List[dict] = Field( + affiliationHistory: List[str] = Field( description="Temporal affiliation information with dates", default_factory=list, ) + # Provenance tracking + source: Optional[str] = Field( + default=None, + description="Data source: 'gimie', 'llm', 'orcid', 'agent_user_enrichment', 'github_profile'", + ) + # Additional metadata contributionSummary: Optional[str] = Field( description="Summary of contributions to the repository", @@ -90,6 +92,352 @@ class Person(BaseModel): ) ``` +## Affiliation Model with Provenance Tracking + +### Overview +The `Affiliation` model provides structured affiliation tracking with provenance information. It replaces simple string-based affiliations (`List[str]`) with rich metadata that tracks organization identifiers and data sources. + +**Location**: `src/data_models/models.py` + +### Model Definition + +```python +class Affiliation(BaseModel): + """Structured affiliation with provenance tracking""" + + name: str = Field( + description="Organization name (e.g., 'Swiss Data Science Center', 'EPFL')" + ) + organizationId: Optional[str] = Field( + default=None, + description="Organization identifier: ROR ID, GitHub handle, or internal ID" + ) + source: str = Field( + description="Data source: 'gimie', 'orcid', 'agent_org_enrichment', 'agent_user_enrichment', 'github_profile', 'email_domain'" + ) +``` + +### Fields + +**name** (required, str): +- Human-readable organization name +- Examples: "Swiss Data Science Center", "EPFL - École Polytechnique Fédérale de Lausanne", "Hackuarium" +- Must always be a string (not a dict or other object) + +**organizationId** (optional, str): +- Organization identifier from various sources +- Can be: + - **ROR ID**: Full URL (e.g., `https://ror.org/02s376052`) + - **GitHub handle**: Organization handle (e.g., `SwissDataScienceCenter`) + - **Internal ID**: Any system-specific identifier + - **null**: When no identifier is available + +**source** (required, str): +- Tracks where the affiliation data came from +- Valid values: + - `"orcid"` - From ORCID employment records + - `"github_profile"` - From GitHub organization memberships + - `"email_domain"` - Inferred from email domains (@epfl.ch, etc.) + - `"agent_user_enrichment"` - From user enrichment AI agent + - `"agent_org_enrichment"` - From organization enrichment AI agent + - `"gimie"` - From GIMIE repository metadata + +### Usage Examples + +```python +from src.data_models import Affiliation, Person + +# Create affiliations with different sources +aff_orcid = Affiliation( + name="EPFL - École Polytechnique Fédérale de Lausanne", + organizationId="https://ror.org/02s376052", + source="orcid" +) + +aff_github = Affiliation( + name="Swiss Data Science Center", + organizationId="SwissDataScienceCenter", + source="github_profile" +) + +aff_email = Affiliation( + name="EPFL", + organizationId=None, + source="email_domain" +) + +# Create person with affiliations +person = Person( + name="Carlos Vivar Rios", + affiliations=[aff_orcid, aff_github, aff_email] +) + +# Access affiliation details +for aff in person.affiliations: + print(f"{aff.name} - Source: {aff.source}, ID: {aff.organizationId}") +``` + +### Integration Points + +#### 1. ORCID Enrichment (`src/utils/utils.py`) +```python +def get_orcid_affiliations(orcid_id: str) -> List[Affiliation]: + """Returns Affiliation objects from ORCID employment history""" + # Creates Affiliation with source="orcid" + return [ + Affiliation( + name=org_name, + organizationId=None, # ORCID doesn't provide ROR IDs + source="orcid" + ) + for org_name in employment_records + ] +``` + +#### 2. GIMIE Extraction (`src/analysis/repositories.py`) +```python +# Extract affiliations from GIMIE metadata +affiliations.append( + Affiliation( + name=org_name, + organizationId=org_id, # Organization URL or ID from GIMIE + source="gimie" + ) +) +``` + +#### 3. Agent Prompts +Agents receive and return structured affiliation data: +```python +# In prompts (formatted as JSON for LLMs) +"affiliations": [ + { + "name": "EPFL", + "organizationId": "https://ror.org/02s376052", + "source": "orcid" + } +] +``` + +### Deduplication Strategy + +Affiliations are deduplicated by name (case-insensitive): + +```python +# Merge affiliations +existing_names = {aff.name.lower(): aff for aff in person.affiliations} + +for new_aff in new_affiliations: + if new_aff.name.lower() not in existing_names: + person.affiliations.append(new_aff) +``` + +### JSON Serialization + +When passing to `json.dumps()`, convert Pydantic models to dicts: + +```python +import json + +# Convert Person with Affiliations to JSON +person_dict = person.model_dump() +json_str = json.dumps(person_dict, indent=2, default=str) +``` + +### JSON-LD Mapping + +The Affiliation model maps to schema.org Organization in JSON-LD: + +```python +# In PYDANTIC_TO_ZOD_MAPPING +"Affiliation": { + "name": "schema:name", + "organizationId": "schema:identifier", + "source": "imag:source", +} + +# In type_mapping +"Affiliation": "http://schema.org/Organization" +``` + +### Common Issues and Solutions + +#### Issue 1: Nested Organization Dicts in Affiliation.name + +**Problem**: Passing full organization dicts to `Affiliation.name` instead of just the name string. + +**Error**: `Input should be a valid string [type=string_type, input_value={'id': '...', ...}, input_type=dict]` + +**Solution**: Always extract the name string from organization dicts: +```python +# ❌ WRONG +affiliations.append( + Affiliation(name=org_dict, ...) # org_dict is a dict! +) + +# ✅ CORRECT +org_name = org_dict.get("legalName") or org_dict.get("name") +affiliations.append( + Affiliation(name=org_name, ...) # org_name is a string +) +``` + +#### Issue 2: JSON Serialization Errors + +**Problem**: Pydantic models aren't directly JSON serializable. + +**Error**: `TypeError: Object of type Affiliation is not JSON serializable` + +**Solution**: Convert to dict before JSON serialization: +```python +# ❌ WRONG +json.dumps({"authors": person_objects}) + +# ✅ CORRECT +json.dumps({ + "authors": [p.model_dump() for p in person_objects] +}, default=str) +``` + +### Benefits of Structured Affiliations + +1. **Provenance Tracking**: Know exactly where each affiliation came from + ```python + orcid_affs = [a for a in person.affiliations if a.source == "orcid"] + ``` + +2. **Organization Linking**: Track organization identifiers across sources + ```python + epfl_by_ror = [a for a in affiliations if "02s376052" in (a.organizationId or "")] + ``` + +3. **Common Organization Detection**: Find authors sharing organizations + ```python + # Find all authors affiliated with SwissCat+ + for author in repository.author: + for aff in author.affiliations: + if "SwissCat" in aff.name: + print(f"{author.name} is affiliated with {aff.name}") + ``` + +4. **Multi-Source Data Quality**: Same organization from multiple sources + ```python + # EPFL from both ORCID and email + [ + Affiliation(name="EPFL", organizationId="https://ror.org/02s376052", source="orcid"), + Affiliation(name="EPFL", organizationId=None, source="email_domain") + ] + ``` + +### Migration from Legacy Format + +**Old format** (deprecated): +```python +affiliations: List[str] = ["EPFL", "Swiss Data Science Center"] +``` + +**New format** (current): +```python +affiliations: List[Affiliation] = [ + Affiliation(name="EPFL", organizationId="https://ror.org/02s376052", source="orcid"), + Affiliation(name="Swiss Data Science Center", organizationId="SwissDataScienceCenter", source="github_profile") +] +``` + +**This is a breaking change** - cached data and API responses changed format after implementation. + +## Person and Organization Source Tracking + +### Overview +Both `Person` and `Organization` models include a `source` field to track data provenance - where the object was created or enriched from. + +**Location**: `src/data_models/models.py` + +### Source Field Definition + +**Person Model**: +```python +source: Optional[str] = Field( + default=None, + description="Data source: 'gimie', 'llm', 'orcid', 'agent_user_enrichment', 'github_profile'", +) +``` + +**Organization Model**: +```python +source: Optional[str] = Field( + default=None, + description="Data source: 'gimie', 'llm', 'agent_org_enrichment', 'github_profile'", +) +``` + +### Valid Source Values + +**For Person**: +- `"gimie"` - From GIMIE extraction (GitHub/GitLab API) +- `"llm"` - From LLM analysis of repository content +- `"orcid"` - From ORCID enrichment +- `"agent_user_enrichment"` - From user enrichment agent +- `"github_profile"` - From GitHub profile parsing + +**For Organization**: +- `"gimie"` - From GIMIE extraction +- `"llm"` - From LLM analysis +- `"agent_org_enrichment"` - From organization enrichment agent +- `"github_profile"` - From GitHub organization profile + +### Usage Example + +```python +# Creating Person with source +person = Person( + name="John Doe", + orcid="0000-0002-1234-5678", + source="gimie", # Extracted from GitHub API +) + +# Creating Organization with source +org = Organization( + legalName="Swiss Data Science Center", + hasRorId="https://ror.org/01234abcd", + source="agent_org_enrichment", # From ROR enrichment +) + +# Filtering by source +gimie_authors = [p for p in authors if p.source == "gimie"] +llm_authors = [p for p in authors if p.source == "llm"] +``` + +### Benefits of Source Tracking + +1. **Clear Provenance**: Know the origin of each Person/Organization +2. **Data Quality**: Identify which sources provide better data +3. **Debugging**: Trace data issues back to their source +4. **Analytics**: Analyze data quality by source +5. **Consistency**: Matches Affiliation's source tracking pattern + +## Organization Model - Simplified Fields + +The Organization model has been simplified to include only essential fields: + +**Current Fields** (kept): +- `id` - Unique identifier +- `type` - Type discriminator ("Organization") +- `legalName` - Organization's legal name +- `hasRorId` - ROR identifier (Optional[HttpUrl]) +- `organizationType` - Type (university, lab, company, etc.) +- `attributionConfidence` - Confidence score (0.0 to 1.0) +- `source` - Data provenance tracking +- `linkedEntities` - Academic catalog relations + +**Removed Fields** (no longer used): +- ❌ `alternateNames` - Alternate organization names +- ❌ `parentOrganization` - Parent organization name +- ❌ `country` - Organization country +- ❌ `website` - Organization website URL + +**Rationale**: These fields were removed to simplify the model and focus on essential identification and attribution data. ROR ID provides sufficient organization identification, and additional metadata can be fetched from ROR API if needed. + ### Common Pitfalls and Best Practices #### 1. Field Name Matching (Critical!) @@ -139,7 +487,7 @@ class AcademicEntity(BaseModel): description="Direct link to entity", default=None, ) - # Note: externalId field has been removed from AcademicCatalogRelation + # Note: externalId field has been removed from linkedEntitiesRelation ``` **Chain of custody for identifiers**: @@ -169,16 +517,16 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional, List if TYPE_CHECKING: - from .academic_catalog import AcademicCatalogRelation + from .linked_entities import linkedEntitiesRelation class Person(BaseModel): - academicCatalogRelations: Optional[List["AcademicCatalogRelation"]] = Field( + linkedEntities: Optional[List["linkedEntitiesRelation"]] = Field( default_factory=list ) # Then in __init__.py, rebuild models: -from .academic_catalog import AcademicCatalogRelation -Person.model_rebuild(_types_namespace={"AcademicCatalogRelation": AcademicCatalogRelation}) +from .linked_entities import linkedEntitiesRelation +Person.model_rebuild(_types_namespace={"linkedEntitiesRelation": linkedEntitiesRelation}) ``` ### Enums @@ -267,6 +615,173 @@ def validate_website(cls, v): 4. **Fail gracefully**: Return `None` for invalid data rather than raising 5. **Log warnings**: Help debug validation issues +## Email Privacy and Anonymization + +### Overview +The system automatically anonymizes email addresses to protect privacy while preserving domain information for institutional affiliation analysis. Email local parts are hashed using SHA-256, keeping only the domain visible. + +### GitAuthor Model (`src/data_models/repository.py`) + +**Email Anonymization**: +```python +class GitAuthor(BaseModel): + id: str = Field( + default="", + description="SHA-256 hash of email and name combination", + ) + name: str + email: Optional[str] = None + commits: Optional[Commits] = None + + @model_validator(mode="after") + def compute_id(self): + """Compute id as SHA-256 hash of email and name combination.""" + email = self.email or "" + name = self.name or "" + emailname = f"{email}{name}".encode("utf-8") + self.id = hashlib.sha256(emailname).hexdigest() + return self + + def anonymize_email_local_part(self, hash_length: int = 12) -> None: + """ + Replace the local part of the email with a SHA-256 hash while keeping the domain. + + Example: "john.doe@epfl.ch" → "a1b2c3d4e5f6@epfl.ch" + + Args: + hash_length: Number of hexadecimal characters to keep from the hash. Defaults to 12. + """ +``` + +**Usage**: +- Email anonymization is applied when extracting git authors via `extract_git_authors()` in `src/context/repository.py` +- The `anonymize_email` parameter (default: `True`) controls whether emails are anonymized +- Anonymization happens **after** the `id` is computed, so the `id` is based on the original email+name combination +- The anonymized email is what gets stored in the model and returned via API + +**Example**: +```python +# In extract_git_authors() +git_author = GitAuthor(name=name, email=email, commits=commits) +if anonymize_email: + git_author.anonymize_email_local_part() +# Result: email is now "a1b2c3d4e5f6@epfl.ch" (local part hashed) +``` + +### Person Model (`src/data_models/models.py`) + +**Email Anonymization**: +```python +class Person(BaseModel): + emails: Optional[List[str]] = Field( + description="Email address(es) - can be a single string or a list of strings", + default_factory=list, + ) + + def anonymize_emails(self, hash_length: int = 12) -> None: + """ + Replace the local part of each email with a SHA-256 hash while keeping the domain. + + Example: ["john.doe@epfl.ch", "jane@ethz.ch"] → ["a1b2c3d4e5f6@epfl.ch", "f6e5d4c3b2a1@ethz.ch"] + + Args: + hash_length: Number of hexadecimal characters to keep from the hash. Defaults to 12. + """ + if not self.emails: + return + + anonymized_emails: list[str] = [] + for email in self.emails: + if not email or "@" not in email: + anonymized_emails.append(email) + continue + + local_part, domain = email.split("@", 1) + if not domain: + anonymized_emails.append(email) + continue + + hashed_local = hashlib.sha256(local_part.encode("utf-8")).hexdigest() + if hash_length > 0: + hashed_local = hashed_local[:hash_length] + + anonymized_emails.append(f"{hashed_local}@{domain}") + + self.emails = anonymized_emails + + @model_validator(mode="after") + def anonymize_emails_after_validation(self): + """ + Automatically anonymize emails after Person model validation to ensure privacy. + This ensures all Person objects have anonymized emails when returned via API. + """ + self.anonymize_emails() + return self +``` + +**Key Features**: +- **Automatic anonymization**: Emails are automatically anonymized after model validation via `model_validator(mode="after")` +- **Multiple emails**: Handles lists of email addresses, anonymizing each one +- **Domain preservation**: Keeps the domain intact (e.g., `@epfl.ch`, `@ethz.ch`) for institutional analysis +- **Hash length**: Default 12 hexadecimal characters (configurable via `hash_length` parameter) +- **Graceful handling**: Skips invalid emails (no `@` symbol, missing domain) + +**Privacy Guarantee**: +- All `Person` objects returned via API endpoints will have anonymized emails +- The original email local parts are never exposed in API responses +- Domain information is preserved for institutional affiliation analysis + +### Privacy Implementation Details + +**When Anonymization Occurs**: +1. **GitAuthor**: During git author extraction (`extract_git_authors()`), if `anonymize_email=True` (default) +2. **Person**: Automatically after model validation via `model_validator(mode="after")` + +**What Gets Anonymized**: +- Email local parts (everything before `@`) +- Domain is preserved (everything after `@`) + +**What Doesn't Get Anonymized**: +- Domain names (needed for institutional analysis) +- Email addresses without `@` symbol (skipped) +- Invalid email formats (skipped) + +**Hash Algorithm**: +- SHA-256 hash of the local part +- First 12 hexadecimal characters used by default +- Configurable via `hash_length` parameter + +**Example Transformations**: +``` +"john.doe@epfl.ch" → "a1b2c3d4e5f6@epfl.ch" +"jane.smith@ethz.ch" → "f6e5d4c3b2a1@ethz.ch" +"admin@example.com" → "c3b2a1f6e5d4@example.com" +``` + +### Best Practices + +1. **Always use anonymization**: Keep `anonymize_email=True` in `extract_git_authors()` for privacy +2. **Don't disable for production**: Email anonymization should always be enabled in production APIs +3. **Domain analysis**: Use anonymized emails for domain-based institutional analysis (domains are preserved) +4. **ID computation**: For `GitAuthor`, the `id` field is computed from the **original** email+name before anonymization, ensuring consistent IDs +5. **API responses**: All API responses will contain anonymized emails automatically (no additional steps needed) + +### Testing + +When testing email anonymization: +```python +# Test GitAuthor anonymization +author = GitAuthor(name="John Doe", email="john.doe@epfl.ch") +author.anonymize_email_local_part() +assert author.email.startswith("a") # Hashed local part +assert author.email.endswith("@epfl.ch") # Domain preserved + +# Test Person anonymization (automatic) +person = Person(name="Jane Smith", emails=["jane@epfl.ch", "jane.smith@ethz.ch"]) +# Emails are automatically anonymized after validation +assert all("@" in email and not email.split("@")[0].contains(".") for email in person.emails) +``` + ## Model Conversion ### JSON-LD Conversion diff --git a/.cursor/rules/simplified-models.mdc b/.cursor/rules/simplified-models.mdc new file mode 100644 index 0000000..45587c8 --- /dev/null +++ b/.cursor/rules/simplified-models.mdc @@ -0,0 +1,609 @@ +--- +alwaysApply: true +--- +# Simplified Model Generation + +## Overview + +Simplified models are lightweight Pydantic models that use only primitive types (strings, numbers, lists, dicts) to be compatible with LLM agents that don't support complex Pydantic types like `HttpUrl`, `date`, or `Enum`. + +**Purpose:** +- Enable LLM agents to work with structured data without complex type validation +- Automatically extract field descriptions from source models +- Maintain consistency between source and simplified models +- Reduce code duplication through automatic generation + +## Architecture + +### Source Models +- **Location**: `src/data_models/repository.py` +- **Main Models**: `SoftwareSourceCode`, `Person`, `GitAuthor`, `Organization` +- **Key Feature**: All fields use `Field(description=...)` for automatic extraction + +### Simplified Models +- **Location**: `src/agents/atomic_agents/models.py` +- **Models**: `SimplifiedRepositoryOutput`, `SimplifiedAuthor`, `SimplifiedGitAuthor` +- **Usage**: Used by the structured output agent in the atomic agent pipeline + +### Generator Utility +- **Location**: `src/data_models/simplified_generator.py` (to be created) +- **Purpose**: Automatically generate simplified models from source models +- **Approach**: Hybrid (code generation + runtime creation support) + +## Type Transformations + +### Automatic Type Simplification + +The generator automatically transforms complex types to primitives: + +| Source Type | Simplified Type | Notes | +|------------|----------------|-------| +| `HttpUrl` | `str` | URLs as plain strings | +| `date` | `str` | ISO format (YYYY-MM-DD) | +| `datetime` | `str` | ISO format with time | +| `Enum` | `str` | Enum value as string | +| `Optional[HttpUrl]` | `Optional[str]` | Preserves Optional wrapper | +| `List[HttpUrl]` | `List[str]` | Preserves List wrapper | +| `Union[Person, Organization]` | `Dict[str, Any]` | Complex unions become dicts | +| Nested `BaseModel` | Simplified version | Recursive simplification | + +### Field Filtering + +Only selected fields are included in simplified models: + +**SimplifiedRepositoryOutput** includes: +- `name`, `applicationCategory`, `codeRepository`, `dateCreated` +- `license`, `author`, `gitAuthors` +- `discipline`, `disciplineJustification` +- `repositoryType`, `repositoryTypeJustification` + +**SimplifiedAuthor** includes: +- `name`, `emails`, `orcid`, `affiliations` + +**SimplifiedGitAuthor** includes: +- `name`, `email`, `commits` + +**SimplifiedOrganization** includes: +- `name`: Organization name (uses `name` not `legalName` for LLM compatibility) +- `organizationType`: Required - type of organization (Research Institute, University, Company, etc.) +- `id`: Optional - organization identifier (GitHub URL, website, etc.) +- `attributionConfidence`: Optional - confidence score (0.0-1.0) for the organization's relationship + +**Important**: `SimplifiedOrganization` uses `name` (not `legalName`) because LLMs naturally produce `name` fields. The conversion code in `repositories.py` maps `name` → `legalName` when creating full `Organization` objects. + +## Implementation Pattern + +### Generator Function + +```python +from pydantic import BaseModel, Field, create_model +from typing import get_origin, get_args, Optional, List, Dict, Any +from datetime import date +from pydantic import HttpUrl +from enum import Enum + +def simplify_type(annotation): + """Convert complex types to primitive types.""" + origin = get_origin(annotation) + + if origin is None: + # Handle non-generic types + if annotation == HttpUrl or annotation == date: + return str + if isinstance(annotation, type) and issubclass(annotation, Enum): + return str + if isinstance(annotation, type) and issubclass(annotation, BaseModel): + # Return a simplified version (recursive) + return create_simplified_model(annotation) + return annotation + + # Handle generic types (List, Optional, etc.) + if origin is Optional: + args = get_args(annotation) + if args: + return Optional[simplify_type(args[0])] + return Optional[str] + + if origin is list or origin is List: + args = get_args(annotation) + if args: + return List[simplify_type(args[0])] + return List[str] + + return annotation + +def create_simplified_model( + source_model: type[BaseModel], + field_filter: list[str] = None +): + """Create a simplified version of a Pydantic model.""" + if not issubclass(source_model, BaseModel): + raise ValueError("Source must be a Pydantic BaseModel") + + new_fields = {} + source_fields = source_model.model_fields + + for field_name, field_info in source_fields.items(): + # Filter fields if specified + if field_filter and field_name not in field_filter: + continue + + # Simplify the type annotation + simplified_type = simplify_type(field_info.annotation) + + # Preserve Field description and default + field_kwargs = {} + if field_info.description: + field_kwargs['description'] = field_info.description + if field_info.default is not ...: + field_kwargs['default'] = field_info.default + elif field_info.default_factory is not ...: + field_kwargs['default_factory'] = field_info.default_factory + + new_fields[field_name] = ( + simplified_type, + Field(**field_kwargs) if field_kwargs else ... + ) + + # Create the simplified model + simplified_model = create_model( + f"Simplified{source_model.__name__}", + **new_fields + ) + + return simplified_model +``` + +### Usage Example + +```python +from src.data_models.repository import SoftwareSourceCode, Person, GitAuthor +from src.data_models.simplified_generator import create_simplified_model + +# Define field filters +REPO_FIELDS = [ + "name", "applicationCategory", "codeRepository", + "dateCreated", "license", "author", "gitAuthors", + "discipline", "disciplineJustification", + "repositoryType", "repositoryTypeJustification" +] + +AUTHOR_FIELDS = ["name", "emails", "orcid", "affiliations"] +GIT_AUTHOR_FIELDS = ["name", "email", "commits"] + +# Generate simplified models +SimplifiedRepositoryOutput = create_simplified_model( + SoftwareSourceCode, + field_filter=REPO_FIELDS +) + +SimplifiedAuthor = create_simplified_model( + Person, + field_filter=AUTHOR_FIELDS +) + +SimplifiedGitAuthor = create_simplified_model( + GitAuthor, + field_filter=GIT_AUTHOR_FIELDS +) +``` + +## Code Generation Approach + +### Generating Python Code + +For better IDE support and type checking, generate actual Python code: + +```python +def generate_simplified_model_code( + source_model: type[BaseModel], + field_filter: list[str] = None, + class_name: str = None +) -> str: + """Generate Python code for simplified models.""" + if class_name is None: + class_name = f"Simplified{source_model.__name__}" + + source_fields = source_model.model_fields + code_lines = [ + f"class {class_name}(BaseModel):", + f' """Simplified {source_model.__name__} model with only primitive types."""', + "" + ] + + for field_name, field_info in source_fields.items(): + if field_filter and field_name not in field_filter: + continue + + # Determine simplified type string + simplified_type = get_simplified_type_string(field_info.annotation) + + # Build field definition + field_parts = [f" {field_name}: {simplified_type}"] + + # Add Field() if there's a description or default + field_kwargs = [] + if field_info.description: + field_kwargs.append(f'description="{field_info.description}"') + if field_info.default is not ...: + field_kwargs.append(f"default={repr(field_info.default)}") + elif field_info.default_factory is not ...: + field_kwargs.append(f"default_factory={field_info.default_factory.__name__}") + + if field_kwargs: + field_parts.append(f" = Field({', '.join(field_kwargs)})") + elif field_info.is_required(): + pass # Required field, no default + else: + field_parts.append(" = None") + + code_lines.append("".join(field_parts)) + + return "\n".join(code_lines) +``` + +### Running the Generator + +Create a script to regenerate simplified models: + +```python +# scripts/generate_simplified_models.py +from src.data_models.simplified_generator import generate_simplified_model_code +from src.data_models.repository import SoftwareSourceCode, Person, GitAuthor + +REPO_FIELDS = [...] +AUTHOR_FIELDS = [...] +GIT_AUTHOR_FIELDS = [...] + +# Generate code +repo_code = generate_simplified_model_code(SoftwareSourceCode, REPO_FIELDS) +author_code = generate_simplified_model_code(Person, AUTHOR_FIELDS) +git_author_code = generate_simplified_model_code(GitAuthor, GIT_AUTHOR_FIELDS) + +# Write to file +with open("src/agents/atomic_agents/models.py", "w") as f: + f.write("# Auto-generated from source models\n") + f.write(repo_code) + f.write("\n\n") + f.write(author_code) + f.write("\n\n") + f.write(git_author_code) +``` + +## Integration with Atomic Agents + +### Structured Output Agent + +The structured output agent uses simplified models: + +```python +# src/agents/atomic_agents/structured_output.py +from .models import SimplifiedRepositoryOutput + +async def generate_structured_output( + compiled_context: CompiledContext, + schema: Dict[str, Any], + example: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + # ... + result = await run_agent_with_fallback( + STRUCTURED_OUTPUT_CONFIGS, + prompt, + agent_context, + SimplifiedRepositoryOutput, # ✅ Uses simplified model + STRUCTURED_OUTPUT_SYSTEM_PROMPT, + tools, + ) + # ... +``` + +### Schema Generation + +The `to_simplified_schema()` method extracts descriptions automatically: + +```python +# src/data_models/repository.py +def to_simplified_schema(self) -> dict: + """Generate simplified schema with auto-extracted descriptions.""" + model_fields = self.model_fields + + def get_field_description(field_name: str, default: str = "") -> str: + if field_name in model_fields: + field_info = model_fields[field_name] + if field_info.description: + return field_info.description + return default + + schema = { + "name": { + "type": "string", + "description": get_field_description("name", "Repository name"), + "required": get_field_required("name"), + }, + # ... more fields + } + return schema +``` + +## Handling List Fields with default_factory + +### Problem: LLMs Returning None for List Fields + +LLMs sometimes return `None` for list fields instead of empty lists, causing validation errors: + +``` +pydantic_core._pydantic_core.ValidationError: +affiliationHistory + Input should be a valid array [type=list_type, input_value=None, input_type=NoneType] +``` + +### Solution: Optional Fields in Simplified Models + +**File**: `src/data_models/conversion.py` + +When creating simplified models, fields with `default_factory` are converted to `Optional[type]` with `default=None`: + +```python +def create_simplified_model(source_model: type[BaseModel], ...): + for field_name, field_info in source_fields.items(): + simplified_type = simplify_type(field_info.annotation) + description = field_info.description or "" + default_factory = field_info.default_factory if hasattr(field_info, 'default_factory') else None + + if default_factory is not None: + # Handle default_factory (e.g., default_factory=list) + # For LLM compatibility, convert to Optional with default=None + # This allows LLMs to return None instead of empty lists + # We'll convert None back to empty lists when reconstructing the full model + new_fields[field_name] = ( + Optional[simplified_type], + Field(default=None, description=description), + ) + elif field_info.default is not ...: + # Field has explicit default value + new_fields[field_name] = ( + simplified_type, + Field(default=field_info.default, description=description), + ) + else: + # Required field + new_fields[field_name] = ( + simplified_type, + Field(description=description), + ) +``` + +**Example transformation:** + +```python +# Source model +class Person(BaseModel): + affiliationHistory: List[str] = Field( + default_factory=list, + description="Temporal affiliation information with dates", + ) + +# Simplified model (for LLM) +class SimplifiedPerson(BaseModel): + affiliationHistory: Optional[List[str]] = Field( + default=None, # LLM can return None + description="Temporal affiliation information with dates", + ) +``` + +### Reconstructing Full Models: clean_model_dict + +**File**: `src/analysis/repositories.py` + +When converting simplified LLM output back to full Pydantic models, `None` values are converted back to empty lists/dicts: + +```python +def clean_model_dict(model_dict: dict, model_type: type) -> dict: + """Convert None to empty lists for fields with default_factory=list""" + if not isinstance(model_dict, dict): + return model_dict + + # Check if model_type has model_fields + if hasattr(model_type, "model_fields"): + for field_name, field_info in model_type.model_fields.items(): + if field_name in model_dict and model_dict[field_name] is None: + # Check if field has default_factory and it's callable + if ( + hasattr(field_info, "default_factory") + and field_info.default_factory is not ... + and field_info.default_factory is not None + and callable(field_info.default_factory) + ): + # Convert None to empty value from default_factory + model_dict[field_name] = field_info.default_factory() + return model_dict +``` + +**Usage in reconstruction:** + +```python +def _convert_simplified_to_full(self, simplified_data: dict) -> SoftwareSourceCode: + # Clean up None values before model creation + cleaned_data = clean_model_dict(simplified_data, SoftwareSourceCode) + + # Convert nested objects (authors) + if "authorPerson" in cleaned_data: + author_persons = [] + for author_dict in cleaned_data.get("authorPerson", []): + # Clean author dict before creating Person object + cleaned_author = clean_model_dict(author_dict, Person) + person = Person(**cleaned_author) + author_persons.append(person) + cleaned_data["author"] = author_persons + + # Create full model + return SoftwareSourceCode(**cleaned_data) +``` + +### Benefits + +1. **LLM Compatibility**: LLMs can return `None` without validation errors +2. **Automatic Cleanup**: `clean_model_dict` handles None → empty list conversion +3. **Type Safety**: Full models still enforce list types +4. **Graceful Handling**: No crashes from missing list fields + +### Example Flow + +```python +# 1. LLM receives simplified model schema +# affiliationHistory: Optional[List[str]] = None + +# 2. LLM returns simplified output with None +{ + "name": "John Doe", + "affiliationHistory": None, # LLM didn't provide affiliations +} + +# 3. clean_model_dict converts None to empty list +{ + "name": "John Doe", + "affiliationHistory": [], # Converted based on default_factory +} + +# 4. Full model validation succeeds +Person( + name="John Doe", + affiliationHistory=[], # List as expected +) +``` + +### Critical Checks in clean_model_dict + +The function includes multiple safety checks: + +```python +# 1. Verify default_factory exists +hasattr(field_info, "default_factory") + +# 2. Ensure it's not ellipsis (...) +field_info.default_factory is not ... + +# 3. Ensure it's not None +field_info.default_factory is not None + +# 4. Ensure it's callable +callable(field_info.default_factory) +``` + +Without these checks, calling `field_info.default_factory()` could raise `TypeError: 'NoneType' object is not callable`. + +## Best Practices + +### 1. Always Use Field Descriptions + +**CRITICAL**: All fields in source models must have `Field(description=...)`: + +```python +# ✅ CORRECT +name: Optional[str] = Field( + default=None, + description="Repository name", +) + +# ❌ WRONG - No description +name: Optional[str] = None +``` + +### 2. Keep Field Filters Updated + +When adding new fields to source models that should be in simplified models: + +1. Update the field filter list +2. Regenerate simplified models +3. Update `to_simplified_schema()` if needed + +### 3. Test Type Transformations + +Verify that complex types are correctly simplified: + +```python +def test_type_simplification(): + """Test that types are correctly simplified.""" + assert simplify_type(HttpUrl) == str + assert simplify_type(date) == str + assert simplify_type(Optional[HttpUrl]) == Optional[str] + assert simplify_type(List[HttpUrl]) == List[str] +``` + +### 4. Preserve Required Fields + +Ensure required fields remain required in simplified models: + +```python +# Source model +repositoryType: RepositoryType = Field(description="Repository type") + +# Simplified model (auto-generated) +repositoryType: str = Field(description="Repository type") # Still required +``` + +### 5. Handle Nested Models Recursively + +When a field contains a nested model, generate a simplified version: + +```python +# Source: author: Optional[List[Person]] +# Simplified: author: Optional[List[SimplifiedAuthor]] +``` + +## Migration Guide + +### When to Regenerate + +Regenerate simplified models when: +- Adding new fields to source models +- Changing field descriptions in source models +- Modifying field types in source models +- Adding new simplified model types + +### Regeneration Process + +1. Update source model fields with descriptions +2. Update field filter lists if needed +3. Run generator script: `python scripts/generate_simplified_models.py` +4. Review generated code +5. Test with atomic agents +6. Commit changes + +### Backward Compatibility + +- Simplified models should remain backward compatible +- Adding new optional fields is safe +- Removing fields requires migration +- Changing field types requires validation updates + +## Common Issues + +### Issue: Missing Field Descriptions + +**Symptom**: Generated models have no descriptions +**Solution**: Add `Field(description=...)` to source model fields + +### Issue: Type Not Simplified + +**Symptom**: Complex types remain in simplified model +**Solution**: Update `simplify_type()` function to handle the type + +### Issue: Nested Model Not Simplified + +**Symptom**: Nested models still use complex types +**Solution**: Ensure recursive simplification in `create_simplified_model()` + +### Issue: Required Fields Become Optional + +**Symptom**: Required fields in source become optional in simplified +**Solution**: Check `field_info.is_required()` and preserve in Field definition + +## Future Enhancements + +Potential improvements: +- [ ] Automatic field filter detection based on usage +- [ ] Validation that simplified models match source models +- [ ] Integration with CI/CD for automatic regeneration +- [ ] Support for more complex type transformations +- [ ] Documentation generation from simplified models diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d6d8634..fca2f0b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -13,10 +13,20 @@ RUN apt-get update && apt-get install -y \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* -# Crear usuario no-root con UID/GID que suele usar VS Code (1000:1000) +# Crear usuario no-root con UID/GID que suele usar VS Code (1000:1000). +# TOOD: Take this user out of sudoers if you want to use this in fully agents mode. RUN useradd -ms /bin/bash -u 1000 vscode \ && apt-get update && apt-get install -y sudo \ && echo "vscode ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers +# Gemini CLI +# Please login outside of the container and copy your credentials to ~/.gemini/... +RUN curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - && sudo apt-get install -y nodejs +RUN npm install -g @google/gemini-cli + +RUN mkdir -p /app/data \ + && chown -R 1000:1000 /app/data \ + && chmod 775 /app/data + USER vscode WORKDIR /workspaces diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2ed66a1..59d883b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -11,13 +11,12 @@ ], "remoteUser": "vscode", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", - "customizations": { "vscode": { - "settings": { - "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", - "python.envFile": "${workspaceFolder}/.env" - }, + "settings": { + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "python.envFile": "${workspaceFolder}/.env" + }, "extensions": [ "ms-python.python", "ms-python.vscode-pylance", @@ -27,7 +26,8 @@ ] } }, - - "forwardPorts": [1234], + "forwardPorts": [ + 1234 + ], "postCreateCommand": "rm -rf .venv && uv venv && uv pip install -e .[dev] && echo '. $PWD/.venv/bin/activate' >> /home/vscode/.bashrc" -} +} \ No newline at end of file diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md new file mode 100644 index 0000000..a164a25 --- /dev/null +++ b/.gemini/GEMINI.md @@ -0,0 +1,179 @@ +# Git Metadata Extractor - Developer Onboarding + +This document provides a comprehensive overview of the `git-metadata-extractor` project, its architecture, and development conventions. It is intended for developers who are new to the project. + +## 1. Project Overview + +The Git Metadata Extractor is a tool designed to analyze software repositories, extract metadata, and enrich it using a series of AI-powered agents. The primary goal is to produce high-quality, structured metadata in JSON-LD format, aligned with schemas used by the EPFL Imaging Plaza and Open Pulse projects. + +**Core Features:** +- Extracts basic metadata using `gimie`. +- Clones repositories to analyze content (READMEs, source code, configs). +- Uses a pipeline of AI agents (`pydantic-ai`) for analysis and enrichment. +- Enriches data with external sources like ORCID, ROR, and EPFL's Infoscience repository. +- Provides a final, holistic assessment of a repository's relationship with EPFL. +- Exposes functionality via a CLI and a FastAPI web server. + +## 2. Architecture + +The application follows a modular, pipeline-based architecture. The core logic is orchestrated by "analysis" classes (`Repository`, `User`, `Organization`) that execute a sequence of steps. + +### Directory Structure + +The `src/` directory is organized by function: + +- `src/api.py`: FastAPI application entry point. +- `src/main.py`: CLI application entry point. +- `src/analysis/`: High-level orchestrators (`Repository`, `User`, `Organization` classes) that manage the analysis pipeline. +- `src/agents/`: Contains all AI agent logic. Each agent has a dedicated file and a `_prompts.py` file for its prompts. +- `src/data_models/`: The single source of truth for all Pydantic data models. All models are exported via `src/data_models/__init__.py`. +- `src/context/`: Provides data from external sources (e.g., `infoscience.py` for the EPFL academic catalog, `repository.py` for cloning and content extraction). +- `src/llm/`: Manages LLM configurations, allowing for multiple providers (OpenAI, OpenRouter, Ollama) with fallback mechanisms. +- `src/cache/`: Implements the SQLite-based caching system. +- `src/parsers/`: Handles parsing of raw data from sources like the GitHub API. + +### Data Analysis Flow (Repository) + +The analysis pipeline for a repository is a key concept: + +1. **Clone & Extract (`src/context/repository.py`):** The repository is cloned, and relevant files (code, docs, configs) are extracted into a markdown format. +2. **GIMIE Analysis (`src/gimie_utils/`):** Basic metadata is extracted using `gimie`. +3. **Initial LLM Analysis (`src/agents/repository.py`):** The main repository agent analyzes the extracted content to produce a `SoftwareSourceCode` Pydantic model. +4. **Enrichment Stages:** + - **ORCID Enrichment:** Author data is enriched with public ORCID information. + - **User Enrichment (`src/agents/user_enrichment.py`):** Analyzes git authors and ORCID data to create detailed `Person` profiles. + - **Organization Enrichment (`src/agents/organization_enrichment.py`):** Identifies and standardizes organizational affiliations using ROR. + - **Academic Catalog Enrichment (`src/agents/linked_entities_enrichment.py`):** Searches academic catalogs like Infoscience for related publications, authors, and labs. +5. **Final EPFL Assessment (`src/agents/epfl_assessment.py`):** A final, holistic agent reviews all collected data to make a definitive, evidence-based assessment of the repository's relationship to EPFL, calculating a confidence score. +6. **Validation & Caching:** The final, enriched data is validated against the Pydantic models and cached in the SQLite database. + +## 3. Key Concepts & Patterns + +### AI Agents (`pydantic-ai`) + +- The core of the enrichment logic is built on `pydantic-ai`. +- **Schema Enforcement is CRITICAL:** Every agent call specifies a Pydantic model as its `output_type`. This forces the LLM to return structured, validated data. **Never use a generic `Dict` as an output type.** +- **Agent Organization:** Agents are located in `src/agents/`. Each has a main implementation file, a `_prompts.py` file, and may use tools from `src/agents/tools.py` or `src/context/`. +- **Structured Output:** Agents that search for multiple items (e.g., academic catalog) return structured dictionaries keyed by the exact input name, eliminating the need for fuzzy name matching in the Python code. + +### Pydantic Models (`src/data_models/`) + +- The project uses **Pydantic V2**. +- All data models are defined in `src/data_models/`. This provides a single source of truth for the application's data structures. +- **Type Discrimination:** A `type` field (`"Person"` or `"Organization"`) is used to distinguish between different entity types in mixed lists, such as the `author` field. +- **Field Naming:** Pydantic models use `camelCase` for field names to align with the target JSON-LD schema. +- **Validation:** Models include built-in validators (e.g., for ORCID and ROR IDs) to normalize data at the point of creation. + +### Configuration (`src/llm/model_config.py`) + +- The application supports multiple LLM providers (OpenAI, OpenRouter, Ollama, and any OpenAI-compatible endpoint). +- Configurations for each analysis type (e.g., `run_llm_analysis`, `run_user_enrichment`) are defined in `MODEL_CONFIGS`. +- Each configuration is a list of models, providing a fallback mechanism if the primary model fails. +- Configurations can be overridden at runtime using environment variables (e.g., `LLM_ANALYSIS_MODELS`). + +### Token & Usage Tracking + +- **Dual Tracking System:** The application tracks token usage in two ways: + 1. **API-Reported:** Official token counts from the LLM provider's API response. + 2. **Client-Side Estimation:** A fallback mechanism using `tiktoken` for validation and for models that don't report usage. +- **`APIStats` Model:** All API endpoints that perform analysis return a `stats` object containing detailed token counts, request duration, and status. +- **Accumulation:** The `Repository`, `User`, and `Organization` analysis classes accumulate token usage across all agent calls in an analysis pipeline. + +## 4. Development Setup + +### Prerequisites +- Python >= 3.9 +- `just` (a command runner, `pip install just`) + +### Installation Steps + +1. **Clone the repository.** +2. **Install `uv`:** This project uses `uv` for fast dependency management. + ```bash + pip install uv + ``` +3. **Set up the development environment:** This command will create a virtual environment, install all dependencies (including dev dependencies), and create a `.env` file from the template. + ```bash + just setup + ``` +4. **Configure Environment Variables:** Edit the newly created `.env` file and add your API keys. + ```env + # Required + OPENAI_API_KEY=sk-... + OPENROUTER_API_KEY=sk-or-... + GITHUB_TOKEN=ghp_... + + # Optional: For EPFL's internal model endpoint + RCP_TOKEN=... + ``` +5. **Install Pre-commit Hooks:** This is a **mandatory** step to ensure code quality. + ```bash + just pre-commit-install + ``` + +## 5. Running the Application + +### CLI Mode +For one-off analysis of a single repository. +```bash +# Basic usage +python src/main.py --url https://github.com/user/repo + +# Using the just command +just extract https://github.com/user/repo +``` + +### API Server Mode +For development, run the FastAPI server with auto-reload. +```bash +# Start the development server +just serve-dev + +# Access the API documentation +# Swagger UI: http://localhost:1234/docs +# ReDoc: http://localhost:1234/redoc +``` + +### Docker +The recommended way to run the application in production or for isolated development. +```bash +# Build the Docker image +just docker-build + +# Run in development mode (with live code-reloading) +just docker-dev + +# Run in production mode +just docker-run +``` + +## 6. Coding Standards & Conventions + +### The Golden Rule: Pre-commit +**All code MUST pass pre-commit checks before being committed.** The hooks are installed via `just pre-commit-install` and will run automatically on `git commit`. You can also run them manually: +```bash +# Run all checks on all files +just pre-commit +``` + +### Linting & Formatting +- The project uses **Ruff** for both linting and formatting. +- Configuration is in `pyproject.toml`. +- **Format code:** `just format-ruff` +- **Check for linting issues:** `just lint` +- **Attempt to auto-fix issues:** `just lint-fix` + +### Testing +- The project uses **Pytest**. +- Tests are located in the `tests/` directory. +- **Run all tests:** `just test` +- **Run tests with coverage:** `just test-coverage` + +### Naming and Style +- Follow standard Python conventions (PEP8). +- **Modules:** `snake_case.py` +- **Classes:** `PascalCase` +- **Functions/Methods:** `snake_case` +- **Type Hints:** Use type hints for all function signatures. +- **Imports:** Use absolute imports from `src`. Ruff will handle sorting. +- **Pydantic Fields:** Use `camelCase` to match the JSON-LD schema. diff --git a/.gitignore b/.gitignore index dc7ed4c..5df4a1e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .ruff_cache data api_cache.db +.env2 # Byte-compiled / optimized / DLL files __pycache__/ @@ -167,3 +168,7 @@ cython_debug/ #.idea/ else/ +DeepLabCutDeepLabCut.json +MalloryWittwer.json +output.jsonld +sdsc-ordes.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..11db740 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,159 @@ +# Agent Operating Guide for git-metadata-extractor + +## Title + Purpose +This guide defines the operating contract for autonomous and semi-autonomous coding agents working in this repository. +The goal is safe, reproducible contributions with minimal human back-and-forth. + +## Project Snapshot +- Language/runtime: Python project with package code under `src/`. +- Main runtime surfaces: + - API: `src/api.py` + - CLI: `src/main.py` + - Analysis orchestration: `src/analysis/` + - Agent pipelines: `src/agents/` + - Data models/contracts: `src/data_models/` + - Tests: `tests/` +- Core references: + - `README.md` + - `docs/AGENT_STRATEGY.md` + +## Environment & Prerequisites +Required environment variables (from `.env.dist` and `.env.example`): +- `OPENAI_API_KEY` +- `OPENROUTER_API_KEY` +- `GITHUB_TOKEN` +- `GITLAB_TOKEN` +- `INFOSCIENCE_TOKEN` +- `MODEL` +- `PROVIDER` +- `SELENIUM_REMOTE_URL` +- `CACHE_DB_PATH` +- `MAX_SELENIUM_SESSIONS` +- `MAX_CACHE_ENTRIES` +- `GUNICORN_CMD_ARGS` + +Rules: +- Never print, log, or commit secrets. +- Never modify secret-bearing files (`.env`, `.env2`, similar secret files) unless explicitly asked. +- If required variables are missing for the requested task, fail fast and report exactly which variables are missing. + +## Canonical Commands +`justfile` is the source of truth for routine operations. Prefer `just` commands over ad-hoc shell commands when equivalent recipes exist. + +- Setup: + - `just install-dev` + - `just setup` +- Run API: + - `just serve-dev` + - `just serve` +- Tests: + - `just test` + - `just test-file tests/.py` +- Quality: + - `just lint` + - `just type-check` + - `just check` +- CI-like local validation: + - `just ci` + +## Architecture Map For Agents +- Repository analysis flow entrypoints: `src/analysis/repositories.py` +- User and organization analysis entrypoints: + - `src/analysis/user.py` + - `src/analysis/organization.py` +- Agent implementations: + - `src/agents/` + - Atomic subpipeline: `src/agents/atomic_agents/` +- Data contracts: + - `src/data_models/` +- Context and external lookups: + - `src/context/` +- Cache layer: + - `src/cache/` + +## Editing Rules (Strict) +- Keep diffs minimal and scoped to the requested task. +- Preserve existing code style, project conventions, and import patterns. +- Do not rename or move public modules unless explicitly requested. +- Do not modify `.env`, `.env2`, or other secret-bearing files unless explicitly requested. +- Never run destructive git/file operations unless explicitly requested. +- If unrelated local changes exist, do not revert them; work around them and report context in the completion summary. + +## Task Playbooks +### Bug Fix Playbook +1. Reproduce the issue with a targeted test (or nearest equivalent validation). +2. Patch the minimal root cause. +3. Run focused tests first; run broader checks if shared paths were touched. +4. Report behavior change and residual risk. + +### Feature Playbook +1. Identify API/data-model impact before coding. +2. Implement required model, pipeline, and endpoint wiring. +3. Add or adjust tests in `tests/`. +4. Validate with `just test` plus relevant lint/type checks. + +### Refactor Playbook +1. Preserve behavior unless behavior change is explicitly requested. +2. Keep API contracts stable. +3. Prove parity with tests and checks. + +## Testing & Validation Requirements +Minimum before completion: +- Run the nearest relevant tests. +- Run lint/type checks for touched Python modules when feasible. + +If validation cannot be completed (missing dependencies, missing env vars, time constraints, external service constraints), report: +- What was attempted. +- What failed and why. +- The exact command(s) to run later. + +## API/Schema Change Rules +For changes to FastAPI endpoints in `src/api.py`, include: +- Updated request/response behavior notes. +- Compatibility or migration notes. +- Test coverage for changed endpoint behavior. + +For changes to models in `src/data_models/`, include: +- Impact notes on downstream usage (`src/analysis/`, `src/agents/`, API surface). +- Tests for new/changed fields and validation behavior. + +## Output/Reporting Contract For Agents +Completion reports must include: +- Files changed +- Behavior change +- Commands run and key results +- Risks / follow-ups + +No vague "done" messages. Reports must include verifiable evidence. + +## Definition of Done +- Requested scope implemented. +- Relevant tests/checks passed, or blockers explicitly documented. +- No secret leakage. +- No unrelated mutations. +- No undocumented behavior changes. + +## Test Cases & Scenarios For This Guide +1. Discoverability +- Scenario: A new agent opens the repository root. +- Expectation: `AGENTS.md` is present with quick-start commands and architecture map. + +2. Fail-fast behavior +- Scenario: A task requires `OPENAI_API_KEY`, but it is missing. +- Expectation: Agent halts and reports the missing variable explicitly; no fabricated results. + +3. Workflow consistency +- Scenario: Bug fix in `src/agents/organization_enrichment.py`. +- Expectation: Agent follows the bug-fix playbook and runs targeted tests first. + +4. Safety guardrails +- Scenario: Dirty worktree with unrelated changes. +- Expectation: Agent avoids reverting unrelated files and reports context. + +5. Reporting quality +- Scenario: Agent completes a task. +- Expectation: Final report includes changed files, commands, outcomes, and risks. + +## Public API/Type Impact +- No code/API/type changes are introduced by this document. +- This file defines a repository-local agent policy contract only. diff --git a/docs/ACADEMIC_CATALOG_OPTION_B_IMPLEMENTATION.md b/docs/ACADEMIC_CATALOG_OPTION_B_IMPLEMENTATION.md index 6a367c3..f6262de 100644 --- a/docs/ACADEMIC_CATALOG_OPTION_B_IMPLEMENTATION.md +++ b/docs/ACADEMIC_CATALOG_OPTION_B_IMPLEMENTATION.md @@ -28,19 +28,19 @@ Implemented **Option B**: Academic catalog agent searches for repository, author ## Data Model Changes -### `AcademicCatalogEnrichmentResult` +### `linkedEntitiesEnrichmentResult` **New structured fields:** ```python -class AcademicCatalogEnrichmentResult(BaseModel): - repository_relations: List[AcademicCatalogRelation] = [] +class linkedEntitiesEnrichmentResult(BaseModel): + repository_relations: List[linkedEntitiesRelation] = [] # Publications about the repository/project itself - author_relations: Dict[str, List[AcademicCatalogRelation]] = {} + author_relations: Dict[str, List[linkedEntitiesRelation]] = {} # Keyed by author name as provided: {"Alexander Mathis": [...relations...]} - organization_relations: Dict[str, List[AcademicCatalogRelation]] = {} + organization_relations: Dict[str, List[linkedEntitiesRelation]] = {} # Keyed by org name as provided: {"DeepLabCut": [...relations...]} # Metadata fields... @@ -53,7 +53,7 @@ class AcademicCatalogEnrichmentResult(BaseModel): ```python @property -def relations(self) -> List[AcademicCatalogRelation]: +def relations(self) -> List[linkedEntitiesRelation]: """Combines all relations for backward compatibility.""" return ( list(repository_relations) + @@ -68,7 +68,7 @@ def relations(self) -> List[AcademicCatalogRelation]: **Input:** ```python -enrich_repository_academic_catalog( +enrich_repository_linked_entities( repository_url="https://github.com/DeepLabCut/DeepLabCut", repository_name="DeepLabCut", description="...", @@ -129,25 +129,25 @@ enrich_repository_academic_catalog( ## Assignment Logic -### In `Repository.run_academic_catalog_enrichment()`: +### In `Repository.run_linked_entities_enrichment()`: ```python # 1. Repository-level relations -self.data.academicCatalogRelations = enrichment_data.repository_relations +self.data.linkedEntities = enrichment_data.repository_relations # 2. Author-level relations (direct lookup by name) for author in self.data.author: if author.name in enrichment_data.author_relations: - author.academicCatalogRelations = enrichment_data.author_relations[author.name] + author.linkedEntities = enrichment_data.author_relations[author.name] else: - author.academicCatalogRelations = [] + author.linkedEntities = [] # 3. Organization-level relations (direct lookup by name) for org in self.data.author: # Orgs can be in author list if org.legalName in enrichment_data.organization_relations: - org.academicCatalogRelations = enrichment_data.organization_relations[org.legalName] + org.linkedEntities = enrichment_data.organization_relations[org.legalName] else: - org.academicCatalogRelations = [] + org.linkedEntities = [] ``` **No name matching needed!** The agent uses the exact names we provide as dictionary keys. @@ -215,18 +215,18 @@ Organizations: ["DeepLabCut"] ### Python Assigns: ```python # Repository -repository.academicCatalogRelations = [4 publications about DeepLabCut] +repository.linkedEntities = [4 publications about DeepLabCut] # Author: Alexander Mathis -author1.academicCatalogRelations = author_relations["Alexander Mathis"] +author1.linkedEntities = author_relations["Alexander Mathis"] # = [person profile + 10 publications] # Author: Mackenzie Weygandt Mathis -author2.academicCatalogRelations = author_relations["Mackenzie Weygandt Mathis"] +author2.linkedEntities = author_relations["Mackenzie Weygandt Mathis"] # = [person profile] # Org: DeepLabCut -org.academicCatalogRelations = organization_relations["DeepLabCut"] +org.linkedEntities = organization_relations["DeepLabCut"] # = [] (no EPFL orgunit found) ``` @@ -234,21 +234,21 @@ org.academicCatalogRelations = organization_relations["DeepLabCut"] ```json { "repository": { - "academicCatalogRelations": [ + "linkedEntities": [ "4 publications about DeepLabCut" ] }, "authors": [ { "name": "Alexander Mathis", - "academicCatalogRelations": [ + "linkedEntities": [ "person profile", "10 publications" ] }, { "name": "Mackenzie Weygandt Mathis", - "academicCatalogRelations": [ + "linkedEntities": [ "person profile" ] } @@ -272,7 +272,7 @@ for author in authors: ```python # New: Organized dict, direct lookup if author.name in enrichment_data.author_relations: - author.academicCatalogRelations = enrichment_data.author_relations[author.name] + author.linkedEntities = enrichment_data.author_relations[author.name] ``` ## Testing @@ -292,16 +292,16 @@ curl "http://0.0.0.0:1234/v1/extract/json/https://github.com/DeepLabCut/DeepLabC ## Files Modified ### Data Models: -- `src/data_models/academic_catalog.py` - Added structured fields +- `src/data_models/linked_entities.py` - Added structured fields ### Agent: -- `src/agents/academic_catalog_prompts.py` - Updated output format instructions +- `src/agents/linked_entities_prompts.py` - Updated output format instructions ### Analysis: - `src/analysis/repositories.py` - Simplified assignment logic ### Documentation: -- `ACADEMIC_CATALOG_OPTION_B_IMPLEMENTATION.md` (this file) +- `linked_entities_OPTION_B_IMPLEMENTATION.md` (this file) ## Conclusion diff --git a/docs/ACADEMIC_CATALOG_REFACTOR_SUMMARY.md b/docs/ACADEMIC_CATALOG_REFACTOR_SUMMARY.md index c8a3673..d3adee5 100644 --- a/docs/ACADEMIC_CATALOG_REFACTOR_SUMMARY.md +++ b/docs/ACADEMIC_CATALOG_REFACTOR_SUMMARY.md @@ -23,12 +23,12 @@ Successfully refactored the Infoscience-specific integration into a broader acad - Added `get_entity_by_uuid()` function for direct UUID access - Documented all findings -### ✅ 2. New Data Models (src/data_models/academic_catalog.py) +### ✅ 2. New Data Models (src/data_models/linked_entities.py) **Created:** - `CatalogType` enum: infoscience, openalex, epfl_graph - `EntityType` enum: publication, person, orgunit -- `AcademicCatalogRelation`: Unified relation model with: +- `linkedEntitiesRelation`: Unified relation model with: - `catalogType`: Which catalog (Infoscience, OpenAlex, etc.) - `entityType`: Type of entity (publication, person, orgunit) - `entity`: Full entity details embedded (InfosciencePublication, InfoscienceAuthor, InfoscienceLab, or Dict) @@ -37,7 +37,7 @@ Successfully refactored the Infoscience-specific integration into a broader acad - `externalId`, `matchedOn`: Optional matching metadata - Helper methods: `get_display_name()`, `get_url()`, `to_markdown()` -- `AcademicCatalogEnrichmentResult`: Agent output model with: +- `linkedEntitiesEnrichmentResult`: Agent output model with: - `relations`: List of catalog relations found - `searchStrategy`: Description of search approach - `catalogsSearched`: List of catalogs searched @@ -47,7 +47,7 @@ Successfully refactored the Infoscience-specific integration into a broader acad ### ✅ 3. Updated Core Models -**Replaced `infoscienceEntity`/`infoscienceEntities` with `academicCatalogRelations` in:** +**Replaced `infoscienceEntity`/`infoscienceEntities` with `linkedEntities` in:** - `Person` (src/data_models/models.py) - `Organization` (src/data_models/models.py) - `SoftwareSourceCode` (src/data_models/repository.py) @@ -57,7 +57,7 @@ Successfully refactored the Infoscience-specific integration into a broader acad **Field Structure:** ```python -academicCatalogRelations: Optional[List["AcademicCatalogRelation"]] = Field( +linkedEntities: Optional[List["linkedEntitiesRelation"]] = Field( description="Relations to entities in academic catalogs (Infoscience, OpenAlex, EPFL Graph, etc.)", default_factory=list, ) @@ -84,14 +84,14 @@ academicCatalogRelations: Optional[List["AcademicCatalogRelation"]] = Field( ### ✅ 5. Academic Catalog Enrichment Agent **New Files:** -- `src/agents/academic_catalog_enrichment.py`: Agent implementation -- `src/agents/academic_catalog_prompts.py`: System and contextual prompts +- `src/agents/linked_entities_enrichment.py`: Agent implementation +- `src/agents/linked_entities_prompts.py`: System and contextual prompts **Agent Features:** - **Three specialized enrichment functions:** - - `enrich_repository_academic_catalog()`: For repositories - - `enrich_user_academic_catalog()`: For users - - `enrich_organization_academic_catalog()`: For organizations + - `enrich_repository_linked_entities()`: For repositories + - `enrich_user_linked_entities()`: For users + - `enrich_organization_linked_entities()`: For organizations - **Tools available:** - `search_infoscience_publications_tool` @@ -106,31 +106,31 @@ academicCatalogRelations: Optional[List["AcademicCatalogRelation"]] = Field( - Accept when not found - Be selective and efficient -- **Output:** Returns `AcademicCatalogEnrichmentResult` with structured relations +- **Output:** Returns `linkedEntitiesEnrichmentResult` with structured relations ### ✅ 6. Pipeline Integration **Integrated into analysis classes:** **Repository (src/analysis/repositories.py):** -- Added `run_academic_catalog_enrichment()` method +- Added `run_linked_entities_enrichment()` method - Runs after organization enrichment, before EPFL assessment - Extracts repository name, description, README excerpt -- Stores relations in `data.academicCatalogRelations` +- Stores relations in `data.linkedEntities` - Tracks token usage **User (src/analysis/user.py):** -- Added `run_academic_catalog_enrichment()` method +- Added `run_linked_entities_enrichment()` method - Runs after user enrichment, before EPFL assessment - Extracts username, full name, bio, organizations -- Stores relations in `data.academicCatalogRelations` +- Stores relations in `data.linkedEntities` - Tracks token usage **Organization (src/analysis/organization.py):** -- Added `run_academic_catalog_enrichment()` method +- Added `run_linked_entities_enrichment()` method - Runs after organization enrichment, before EPFL assessment - Extracts org name, description, website, members -- Stores relations in `data.academicCatalogRelations` +- Stores relations in `data.linkedEntities` - Tracks token usage **All integrations:** @@ -177,7 +177,7 @@ curl "http://0.0.0.0:1234/v1/extract/json/https://github.com/DeepLabCut/DeepLabC ``` **What to verify:** -1. `academicCatalogRelations` field exists in output +1. `linkedEntities` field exists in output 2. Relations have `catalogType: "infoscience"` 3. Relations have correct `entityType` (publication, person, orgunit) 4. Entity objects are fully populated with UUIDs and URLs @@ -232,11 +232,11 @@ curl "http://0.0.0.0:1234/v1/extract/json/https://github.com/DeepLabCut/DeepLabC ## Files Created ### New Files: -- `src/data_models/academic_catalog.py` -- `src/agents/academic_catalog_enrichment.py` -- `src/agents/academic_catalog_prompts.py` +- `src/data_models/linked_entities.py` +- `src/agents/linked_entities_enrichment.py` +- `src/agents/linked_entities_prompts.py` - `INFOSCIENCE_API_FINDINGS.md` -- `ACADEMIC_CATALOG_REFACTOR_SUMMARY.md` (this file) +- `linked_entities_REFACTOR_SUMMARY.md` (this file) ### Modified Files: - `src/data_models/models.py` @@ -258,7 +258,7 @@ curl "http://0.0.0.0:1234/v1/extract/json/https://github.com/DeepLabCut/DeepLabC ### ⚠️ API Changes: - **Removed field:** `infoscienceEntity` (singular) from `Person`, `Organization` - **Removed field:** `infoscienceEntities` (plural) from `SoftwareSourceCode`, `GitHubUser`, `GitHubOrganization` -- **Added field:** `academicCatalogRelations` (always plural) to all above models +- **Added field:** `linkedEntities` (always plural) to all above models ### Migration Path: Old code accessing `infoscienceEntity`: @@ -271,8 +271,8 @@ if person.infoscienceEntity: New code: ```python # NEW -if person.academicCatalogRelations: - for relation in person.academicCatalogRelations: +if person.linkedEntities: + for relation in person.linkedEntities: if relation.catalogType == CatalogType.INFOSCIENCE: print(relation.entity.name) ``` @@ -280,7 +280,7 @@ if person.academicCatalogRelations: Helper methods: ```python # Get Infoscience publications -catalog_result = enrichment_result # AcademicCatalogEnrichmentResult +catalog_result = enrichment_result # linkedEntitiesEnrichmentResult infoscience_relations = catalog_result.get_by_catalog(CatalogType.INFOSCIENCE) publications = catalog_result.get_publications() persons = catalog_result.get_persons() diff --git a/docs/AFFILIATION_CHANGES.md b/docs/AFFILIATION_CHANGES.md new file mode 100644 index 0000000..acf863f --- /dev/null +++ b/docs/AFFILIATION_CHANGES.md @@ -0,0 +1,251 @@ +# Enhanced Affiliation Tracking - Implementation Summary + +## Overview +Replaced simple string-based `affiliations: List[str]` with structured `affiliations: List[Affiliation]` throughout the codebase to track organization identifiers and data provenance. + +## Breaking Changes ⚠️ + +This is a **breaking change**. API responses and cached data have changed format: + +### Before (Old Format) +```json +{ + "affiliations": ["EPFL", "Swiss Data Science Center", "Hackuarium"] +} +``` + +### After (New Format) +```json +{ + "affiliations": [ + { + "name": "EPFL", + "organizationId": "https://ror.org/02s376052", + "source": "orcid" + }, + { + "name": "Swiss Data Science Center", + "organizationId": "SwissDataScienceCenter", + "source": "github_profile" + }, + { + "name": "Hackuarium", + "organizationId": null, + "source": "agent_user_enrichment" + } + ] +} +``` + +## New Data Model + +### Affiliation Model +Location: `src/data_models/models.py` + +```python +class Affiliation(BaseModel): + """Structured affiliation with provenance tracking""" + + name: str = Field( + description="Organization name (e.g., 'Swiss Data Science Center', 'EPFL')" + ) + organizationId: Optional[str] = Field( + default=None, + description="Organization identifier: ROR ID, GitHub handle, or internal ID" + ) + source: str = Field( + description="Data source: 'gimie', 'orcid', 'agent_org_enrichment', 'agent_user_enrichment', 'github_profile', 'email_domain'" + ) +``` + +### Source Types +- `orcid` - From ORCID employment records +- `github_profile` - From GitHub organization memberships +- `email_domain` - Inferred from email domains (@epfl.ch, etc.) +- `agent_user_enrichment` - From user enrichment AI agent +- `agent_org_enrichment` - From organization enrichment AI agent +- `gimie` - From GIMIE repository metadata + +### Organization ID Types +- **ROR ID**: Full URL format (e.g., `https://ror.org/02s376052`) +- **GitHub Handle**: Organization handle (e.g., `SwissDataScienceCenter`) +- **Internal ID**: Any internal identifier from source systems +- **null**: When no identifier is available + +## Files Modified + +### 1. Core Data Models +- ✅ `src/data_models/models.py` - Added Affiliation model, updated Person.affiliations +- ✅ `src/data_models/user.py` - Updated EnrichedAuthor.affiliations +- ✅ `src/data_models/__init__.py` - Exported Affiliation model + +### 2. Utilities +- ✅ `src/utils/utils.py` + - Updated `get_orcid_affiliations()` to return `List[Affiliation]` + - Updated `enrich_author_with_orcid()` to handle Affiliation objects + - Merging now uses name-based deduplication + +### 3. Repository Analysis +- ✅ `src/analysis/repositories.py` + - Updated GIMIE affiliation extraction to create Affiliation objects + - Updated affiliation merging logic in `_convert_simplified_to_full()` + - Handles dict and Affiliation object formats + +### 4. Agent Prompts +- ✅ `src/agents/user_prompts.py` + - Updated system prompt to explain Affiliation structure + - Formatted affiliation display in prompts as structured objects +- ✅ `src/agents/organization_prompts.py` + - Updated affiliation display for ORCID authors (2 locations) + - Shows name, organizationId, and source in prompts +- ✅ `src/agents/organization_enrichment.py` + - Updated `_pre_search_ror_for_organizations()` to handle Affiliation objects + - Handles dict, object, and legacy string formats + +### 5. JSON-LD Conversion +- ✅ `src/data_models/conversion.py` + - Added Affiliation to `PYDANTIC_TO_ZOD_MAPPING` + - Added Affiliation to type_mapping + - Mapped fields: name → schema:name, organizationId → schema:identifier, source → imag:source + +### 6. Simplified Models +- ✅ `src/data_models/repository.py` + - Updated `to_simplified_schema()` to extract names from Affiliation objects + - Converts Affiliation objects to simple strings for atomic agents + +## Benefits + +### 1. Provenance Tracking +Now you can see exactly where each affiliation came from: +```python +for aff in person.affiliations: + print(f"{aff.name} - Source: {aff.source}") +``` + +### 2. Organization Linking +Can track organization identifiers (ROR, GitHub handles): +```python +epfl_affs = [aff for aff in person.affiliations if aff.organizationId == "https://ror.org/02s376052"] +``` + +### 3. Common Organization Detection +Can now identify when authors share organizations: +```python +# Find all authors affiliated with SwissCat+ +swisscat_authors = [] +for author in repository.author: + for aff in author.affiliations: + if "SwissCat" in aff.name or aff.organizationId == "SwissCat+": + swisscat_authors.append(author) +``` + +### 4. Multi-Source Enrichment +Same organization from multiple sources is properly tracked: +```python +# EPFL from ORCID +Affiliation(name="EPFL", organizationId="https://ror.org/02s376052", source="orcid") +# EPFL from email +Affiliation(name="EPFL", organizationId=None, source="email_domain") +``` + +### 5. Deduplication +Smart merging prevents duplicates based on organization name (case-insensitive): +```python +existing_names = {aff.name.lower(): aff for aff in person.affiliations} +# Only adds if name doesn't already exist +``` + +## Migration Notes + +### Cache Impact +- **All cached data will be in old format** (List[str]) +- **New analysis will return new format** (List[Affiliation]) +- Recommendation: Clear cache after deployment or add version check + +### API Consumers +API consumers will need to update to handle the new structure: + +**Old code:** +```python +affiliations = person["affiliations"] # List of strings +print(affiliations[0]) # "EPFL" +``` + +**New code:** +```python +affiliations = person["affiliations"] # List of Affiliation objects +print(affiliations[0]["name"]) # "EPFL" +print(affiliations[0]["organizationId"]) # "https://ror.org/02s376052" +print(affiliations[0]["source"]) # "orcid" +``` + +### Backward Compatibility +**None.** This is an intentional breaking change for better data quality. + +## Testing + +To test the implementation with a real repository: + +```bash +# Test with Carlos Vivar Rios' profile +curl "http://0.0.0.0:1234/v1/user/llm/json/github.com/caviri?force_refresh=true" + +# Look for the affiliations field in the response +# Each affiliation should have: name, organizationId, source +``` + +Expected result: +- Affiliations will be objects with provenance information +- GitHub organizations will have their handles as organizationId +- ORCID affiliations will have ROR IDs (when available) +- Source field will indicate where each affiliation came from + +## Future Enhancements + +Potential improvements: +- [ ] Add confidence scores to Affiliation model +- [ ] Add temporal information (start/end dates) +- [ ] Automatic ROR ID lookup for all affiliations +- [ ] Affiliation validation and normalization +- [ ] Affiliation history tracking (separate from affiliations list) +- [ ] Cross-reference with other catalogs (OpenAlex, EPFL Graph) + +## Rollback Plan + +If issues arise, to rollback: +1. Revert changes to `src/data_models/models.py` (Affiliation model and Person.affiliations) +2. Revert changes to `src/utils/utils.py` +3. Revert changes to agent prompts +4. Clear cache to remove mixed-format data +5. Restart server + +## Fixes Applied + +### Issue 1: Nested Organization Objects in Affiliation.name +**Problem**: GIMIE extraction was passing full organization dicts to `Affiliation.name` instead of just the organization name string. + +**Fix** (lines 703-705, 863-892 in `src/analysis/repositories.py`): +- Extract name string from organization dicts: `org_data.get("legalName") or org_data.get("name")` +- Add validation to ensure `name` is always a string +- Recursively extract name if nested dict is encountered +- Log warnings when unexpected data types are found + +### Issue 2: Affiliation Objects Not JSON Serializable +**Problem**: When passing GIMIE data to the atomic LLM pipeline, `json.dumps()` failed because Affiliation (Pydantic) objects aren't directly JSON serializable. + +**Error**: `TypeError: Object of type Affiliation is not JSON serializable` + +**Fix** (lines 119-136 in `src/analysis/repositories.py`): +- Convert Person objects to dicts using `model_dump()` before JSON serialization +- Convert Organization objects to dicts using `model_dump()` before JSON serialization +- Added `default=str` fallback to handle any other non-serializable objects +- This ensures all Pydantic models (including nested Affiliation objects) are properly serialized + +## Questions or Issues? + +If you encounter problems with the new affiliation tracking: +1. Check that all Affiliation objects have required fields (name, source) +2. Verify organizationId is either a string or null (not empty string) +3. Ensure source is one of the valid source types +4. Check logs for validation errors during model creation +5. If you see "Affiliation name is not a string" warnings, check GIMIE extraction logic diff --git a/docs/AGENT_STRATEGY.md b/docs/AGENT_STRATEGY.md new file mode 100644 index 0000000..4edfde1 --- /dev/null +++ b/docs/AGENT_STRATEGY.md @@ -0,0 +1,117 @@ +# Repository Analysis Agent Strategy + +This document outlines the step-by-step analysis pipeline executed by the `Repository` class in `src/analysis/repositories.py`. The strategy involves a sequence of data extraction, AI-powered analysis, and enrichment steps to produce a comprehensive metadata profile for a given software repository. + +## Analysis Pipeline Flowchart + +The following diagram illustrates the complete analysis flow, including optional enrichment steps and the data models used at each stage. + +```mermaid +graph TD + subgraph "Start" + A[Input: Repository URL] + end + + subgraph "Cache & Pre-computation" + B{Cache Check}; + C[run_gimie_analysis]; + end + + subgraph "Core LLM Analysis" + D[run_llm_analysis
Agent: llm_request_repo_infos
DataModel: SoftwareSourceCode]; + E[run_authors_enrichment
(ORCID Scraping)
DataModel: Person]; + end + + subgraph "Optional Enrichments" + F{enrich_users?}; + G[run_user_enrichment
Agent: enrich_users_from_dict
DataModel: UserEnrichmentResult]; + H{enrich_orgs?}; + I[run_organization_enrichment
Agent: enrich_organizations_from_dict
DataModel: OrganizationEnrichmentResult]; + end + + subgraph "Final Assessments" + J[run_linked_entities_enrichment
Agent: enrich_repository_linked_entities
DataModel: linkedEntitiesEnrichmentResult]; + K[run_epfl_final_assessment
Agent: assess_epfl_relationship
DataModel: EPFLAssessmentResult]; + end + + subgraph "Finalization" + L[run_validation]; + M[save_in_cache]; + Y[End: Return Enriched Data]; + Z[End: Return Cached Data]; + end + + %% --- Define Flow --- + A --> B; + B -- Cache Miss / Force Refresh --> C; + B -- Cache Hit --> Z; + + C --> D; + D --> E; + E --> F; + + F -- Yes --> G; + G --> H; + F -- No --> H; + + H -- Yes --> I; + I --> J; + H -- No --> J; + + J --> K; + K --> L; + L --> M; + M --> Y; + + %% --- Style Definitions --- + style A fill:#f9f,stroke:#333,stroke-width:2px + style Z fill:#bfa,stroke:#333,stroke-width.md:2px + style Y fill:#bfa,stroke:#333,stroke-width.md:2px + classDef agentNode fill:#dff,stroke:#333,stroke-width.md:2px + class D,G,I,J,K agentNode +``` + +## Pipeline Steps Explained + +The `Repository.run_analysis` method orchestrates the following steps in sequence: + +1. **Cache Check**: Before any processing, the system checks if a complete, cached result for the given repository URL already exists. If a valid cache entry is found and `force_refresh` is `false`, the cached data is returned immediately, and the pipeline stops. + +2. **GIMIE Analysis (`run_gimie_analysis`)**: + - **Purpose**: Extracts basic, structured metadata from the repository using the `gimie` tool. + - **Output**: A JSON-LD graph which is used as context for the subsequent LLM analysis. + +3. **Core LLM Analysis (`run_llm_analysis`)**: + - **Agent**: `llm_request_repo_infos` + - **Purpose**: This is the main analysis step. The agent receives the repository's content (code, READMEs, etc.) and the GIMIE output. It analyzes this context to generate the initial `SoftwareSourceCode` object. + - **Data Model**: `SoftwareSourceCode` + +4. **Author ORCID Enrichment (`run_authors_enrichment`)**: + - **Purpose**: A non-agent step that iterates through the authors identified by the LLM. If an author has an ORCID iD, this step scrapes their public ORCID profile to add affiliation data. + - **Data Model**: Modifies the `Person` objects within the `SoftwareSourceCode.author` list. + +5. **User Enrichment (`run_user_enrichment`)** - *Optional*: + - **Triggered by**: `enrich_users=true` query parameter. + - **Agent**: `enrich_users_from_dict` + - **Purpose**: Performs a deep analysis of git authors and existing author data. It uses tools to search ORCID and the web to create detailed author profiles, including affiliation history and contribution summaries. + - **Data Model**: The agent returns a `UserEnrichmentResult`, and the `EnrichedAuthor` objects within it are converted to `Person` objects, replacing the existing author list in `self.data`. + +6. **Organization Enrichment (`run_organization_enrichment`)** - *Optional*: + - **Triggered by**: `enrich_orgs=true` query parameter. + - **Agent**: `enrich_organizations_from_dict` + - **Purpose**: Analyzes git author emails and existing organization mentions to identify and standardize institutional affiliations. It uses the ROR (Research Organization Registry) API to fetch canonical data for organizations. + - **Data Model**: The agent returns an `OrganizationEnrichmentResult`. The `Organization` objects from this result replace the `relatedToOrganizations` list in `self.data`. + +7. **Academic Catalog Enrichment (`run_linked_entities_enrichment`)**: + - **Agent**: `enrich_repository_linked_entities` + - **Purpose**: Searches academic catalogs (currently EPFL Infoscience) for publications, researchers, and labs related to the repository, its authors, and its affiliated organizations. + - **Data Model**: Returns an `linkedEntitiesEnrichmentResult`. The `linkedEntitiesRelation` objects are then assigned to the `linkedEntities` fields on the main `SoftwareSourceCode` object as well as on the individual `Person` and `Organization` objects. + +8. **EPFL Final Assessment (`run_epfl_final_assessment`)**: + - **Agent**: `assess_epfl_relationship` + - **Purpose**: This is the final step in the analysis. This agent performs a holistic review of all data collected in the previous steps to make a definitive, evidence-based judgment on the repository's relationship to EPFL. + - **Data Model**: Returns an `EPFLAssessmentResult`. The findings (`relatedToEPFL`, `relatedToEPFLConfidence`, `relatedToEPFLJustification`) overwrite any previous values in `self.data` to ensure consistency. + +9. **Validation & Caching (`run_validation`, `save_in_cache`)**: + - **Purpose**: The final, enriched `SoftwareSourceCode` object is validated against the Pydantic model one last time. If valid, the complete result is saved to the SQLite cache for future requests. + - **Output**: The final, enriched `SoftwareSourceCode` object is returned. diff --git a/docs/ESTIMATED_TOKENS_FIX.md b/docs/ESTIMATED_TOKENS_FIX.md index e59dc3a..ffd1c0c 100644 --- a/docs/ESTIMATED_TOKENS_FIX.md +++ b/docs/ESTIMATED_TOKENS_FIX.md @@ -16,7 +16,7 @@ After comprehensive audit, found **TWO critical bugs** where estimated tokens we ### Bug #1: Academic Catalog Enrichment - Wrong Key Names ❌ -**Location:** `src/agents/academic_catalog_enrichment.py` lines 155-157 +**Location:** `src/agents/linked_entities_enrichment.py` lines 155-157 **Problem:** ```python @@ -175,7 +175,7 @@ if hasattr(result, "usage"): ## Files Modified -1. `src/agents/academic_catalog_enrichment.py` +1. `src/agents/linked_entities_enrichment.py` - Fixed key names: `prompt_tokens` → `input_tokens` - Fixed key names: `completion_tokens` → `output_tokens` diff --git a/docs/JSONLD_CONVERSION_SUMMARY.md b/docs/JSONLD_CONVERSION_SUMMARY.md new file mode 100644 index 0000000..077c516 --- /dev/null +++ b/docs/JSONLD_CONVERSION_SUMMARY.md @@ -0,0 +1,271 @@ +# JSON-LD Conversion Summary + +## Quick Reference: Key Property Mappings + +This document provides a quick reference for the most commonly used Pydantic→JSON-LD property mappings. + +### Core Repository Properties + +| Pydantic Field | JSON-LD Property | Notes | +|----------------|------------------|-------| +| `name` | `schema:name` | Repository name | +| `description` | `schema:description` | Repository description | +| `codeRepository` | `schema:codeRepository` | GitHub/GitLab URL | +| `author` | `schema:author` | List of Person/Organization | +| `license` | `schema:license` | SPDX license URL | +| `discipline` | `pulse:discipline` | Wikidata discipline URIs | +| `repositoryType` | `pulse:repositoryType` | PULSE enum values | + +### Person Properties + +| Pydantic Field | JSON-LD Property | Notes | +|----------------|------------------|-------| +| `name` | `schema:name` | Full name | +| `email` | `pulse:email` | Email address | +| `orcid` | `md4i:orcidId` | ORCID identifier | +| `affiliation` | `schema:affiliation` | Institution/org | +| `academicCatalogRelations` | `pulse:hasAcademicCatalogRelation` | Catalog links | + +### Organization Properties + +| Pydantic Field | JSON-LD Property | Notes | +|----------------|------------------|-------| +| `legalName` | `schema:legalName` | Official name | +| `hasRorId` | `md4i:hasRorId` | ROR identifier URL | +| `website` | `schema:url` | Organization website | + +### Academic Catalog Relations + +| Pydantic Field | JSON-LD Property | Notes | +|----------------|------------------|-------| +| `catalogType` | `pulse:catalogType` | infoscience, orcid, ror, wikidata | +| `entityType` | `pulse:entityType` | person, organization, publication, project | +| `entity` | `pulse:hasCatalogEntity` | The actual entity | +| `confidence` | `pulse:confidence` | 0.0-1.0 | +| `justification` | `pulse:justification` | Why this relation exists | +| `matchedOn` | `pulse:matchedOn` | Fields used for matching | + +## Namespace Prefixes + +```turtle +@prefix schema: . +@prefix sd: . +@prefix pulse: . +@prefix md4i: . +@prefix rdf: . +@prefix rdfs: . +@prefix owl: . +@prefix xsd: . +@prefix wd: . +``` + +## Example JSON-LD Output + +### Repository with Author + +```json +{ + "@context": { + "schema": "http://schema.org/", + "pulse": "https://open-pulse.epfl.ch/ontology#", + "md4i": "http://w3id.org/nfdi4ing/metadata4ing#" + }, + "@graph": [ + { + "@id": "https://github.com/example/my-repo", + "@type": "schema:SoftwareSourceCode", + "schema:name": "My Research Software", + "schema:description": "A tool for scientific computing", + "schema:codeRepository": [ + {"@id": "https://github.com/example/my-repo"} + ], + "schema:license": "https://spdx.org/licenses/MIT", + "schema:author": [ + { + "@type": "schema:Person", + "schema:name": "Jane Doe", + "md4i:orcidId": {"@id": "https://orcid.org/0000-0002-1234-5678"}, + "schema:affiliation": ["EPFL"] + } + ], + "pulse:repositoryType": "pulse:Software", + "pulse:discipline": [ + {"@id": "wd:Q420"} + ] + } + ] +} +``` + +### Person with Academic Catalog Relations + +```json +{ + "@context": { + "schema": "http://schema.org/", + "pulse": "https://open-pulse.epfl.ch/ontology#", + "md4i": "http://w3id.org/nfdi4ing/metadata4ing#" + }, + "@graph": [ + { + "@type": "schema:Person", + "schema:name": "Jane Doe", + "pulse:email": "jane.doe@epfl.ch", + "md4i:orcidId": "0000-0002-1234-5678", + "schema:affiliation": ["EPFL", "CVLAB"], + "pulse:hasAcademicCatalogRelation": [ + { + "@type": "pulse:AcademicCatalogRelation", + "pulse:catalogType": "infoscience", + "pulse:entityType": "person", + "pulse:hasCatalogEntity": { + "@type": "pulse:CatalogEntity", + "pulse:uuid": "abc-123-def", + "schema:name": "Jane Doe", + "pulse:profileUrl": { + "@id": "https://infoscience.epfl.ch/entities/person/abc-123-def" + } + }, + "pulse:confidence": 0.95, + "pulse:justification": "Matched on name and email", + "pulse:matchedOn": ["name", "email"] + } + ] + } + ] +} +``` + +### Organization with ROR + +```json +{ + "@context": { + "schema": "http://schema.org/", + "md4i": "http://w3id.org/nfdi4ing/metadata4ing#" + }, + "@graph": [ + { + "@type": "schema:Organization", + "schema:legalName": "École Polytechnique Fédérale de Lausanne", + "md4i:hasRorId": {"@id": "https://ror.org/02s376052"}, + "schema:url": {"@id": "https://www.epfl.ch"} + } + ] +} +``` + +## Conversion Functions + +### Pydantic → JSON-LD + +```python +from src.data_models.conversion import convert_pydantic_to_jsonld + +# Convert any Pydantic model to JSON-LD +jsonld = convert_pydantic_to_jsonld(pydantic_model, base_url=optional_base_url) +``` + +The function: +1. Automatically detects the model type +2. Maps fields using `PYDANTIC_TO_ZOD_MAPPING` +3. Handles nested models recursively +4. Converts enums to proper values +5. Formats dates as ISO 8601 +6. Converts ORCID IDs to URLs + +### JSON-LD → Pydantic + +```python +from src.data_models.conversion import convert_jsonld_to_pydantic + +# Convert JSON-LD graph to Pydantic model +model = convert_jsonld_to_pydantic(jsonld_graph) +``` + +The function: +1. Parses the `@graph` array +2. Identifies entity types via `@type` +3. Maps JSON-LD properties to Pydantic fields using `JSONLD_TO_PYDANTIC_MAPPING` +4. Resolves nested entity references +5. Validates and constructs Pydantic models + +## Important Notes + +### ORCID Handling + +ORCID identifiers are stored as plain strings in Pydantic (`0000-0002-1234-5678`) but **always** converted to URL format in JSON-LD: + +```json +"md4i:orcidId": {"@id": "https://orcid.org/0000-0002-1234-5678"} +``` + +### Discipline Values + +Disciplines are Wikidata entity URIs: +- Biology: `wd:Q420` +- Mathematics: `wd:Q395` +- Physics: `wd:Q413` +- Computer Engineering: `wd:Q428691` + +Full list in PULSE ontology documentation. + +### Repository Types + +Repository types use PULSE enum values: +- Software: `pulse:Software` +- Educational Resource: `pulse:EducationalResource` +- Documentation: `pulse:Documentation` +- Data: `pulse:Data` +- Other: `pulse:Other` + +### Confidence Scores + +All confidence scores must be between 0.0 and 1.0 (inclusive). Used for: +- `pulse:confidence` in academic catalog relations +- `pulse:relatedToEPFLConfidence` +- `Organization.attributionConfidence` + +### Justification Fields + +Multiple fields map to `pulse:justification`: +- `disciplineJustification` +- `repositoryTypeJustification` +- `relatedToOrganizationJustification` +- `relatedToEPFLJustification` +- `AcademicCatalogRelation.justification` + +These are kept separate in Pydantic for context but may be merged in JSON-LD serialization. + +## Validation + +The PULSE ontology includes SHACL shapes for validation. Key rules: + +1. **Required fields**: Many properties are marked `sh:minCount 1` +2. **Pattern constraints**: URLs must match `^http.*` +3. **Length constraints**: `schema:name` has `sh:maxLength 60` +4. **Cardinality**: Some fields are `sh:maxCount 1` +5. **Enumerations**: `catalogType`, `entityType`, etc. have fixed value lists + +Run SHACL validation after conversion to ensure compliance. + +## Migration Notes + +### Changes from imaging-plaza to PULSE + +Key namespace changes: +- `imag:` → `pulse:` for custom properties +- `md4i:orcid` → `md4i:orcidId` +- Added academic catalog relation support +- Added Wikidata discipline mappings + +### Deprecated Properties + +- `imag:infoscienceEntities` → Use `pulse:hasAcademicCatalogRelation` +- `imag:relatedToOrganization` → `pulse:relatedToOrganization` + +## See Also + +- [Full Mapping Documentation](./PYDANTIC_JSONLD_MAPPING.md) +- [PULSE Ontology](https://open-pulse.epfl.ch/ontology#) +- [Academic Catalog Integration](./ACADEMIC_CATALOG_OPTION_B_IMPLEMENTATION.md) diff --git a/docs/JSONLD_MAPPING_UPDATE.md b/docs/JSONLD_MAPPING_UPDATE.md new file mode 100644 index 0000000..578e08c --- /dev/null +++ b/docs/JSONLD_MAPPING_UPDATE.md @@ -0,0 +1,290 @@ +# JSON-LD Mapping Update - PULSE Ontology Integration + +## Summary + +Updated the Pydantic→JSON-LD mapping system to align with the PULSE (EPFL Open Science) ontology. This enables proper semantic representation of research software metadata in RDF/JSON-LD format. + +## Changes Made + +### 1. Updated Namespace Prefixes + +**File**: `src/data_models/conversion.py` + +Changed from `imaging-plaza` to `pulse` ontology: + +```python +# Before +context = { + "schema": "http://schema.org/", + "sd": "https://w3id.org/okn/o/sd#", + "imag": "https://imaging-plaza.epfl.ch/ontology/", + "md4i": "https://w3id.org/md4i/", +} + +# After +context = { + "schema": "http://schema.org/", + "sd": "https://w3id.org/okn/o/sd#", + "pulse": "https://open-pulse.epfl.ch/ontology#", + "md4i": "http://w3id.org/nfdi4ing/metadata4ing#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "owl": "http://www.w3.org/2002/07/owl#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "dcterms": "http://purl.org/dc/terms/", + "wd": "http://www.wikidata.org/entity/", +} +``` + +### 2. Extended Property Mappings + +**File**: `src/data_models/conversion.py` + +#### Added New Models +- `AcademicCatalogRelation`: Links to academic catalogs (Infoscience, ORCID, ROR, Wikidata) +- `CatalogEntity`: Entities from academic catalogs +- `InfosciencePublication`: Publications from EPFL's Infoscience +- `InfoscienceAuthor`: Authors from Infoscience +- `InfoscienceLab`: Labs/orgunits from Infoscience +- `GitHubOrganization`: GitHub org with enriched metadata + +#### Updated Existing Models +- **Person**: Added `academicCatalogRelations`, `gitAuthorIds`, `affiliationHistory`, etc. +- **Organization**: Added `academicCatalogRelations` +- **SoftwareSourceCode**: Added `academicCatalogRelations`, updated property mappings + +### 3. Property Mapping Updates + +Key changes in `PYDANTIC_TO_ZOD_MAPPING`: + +| Old Property | New Property | Model | +|--------------|--------------|-------| +| `imag:confidence` | `pulse:confidence` | All | +| `imag:justification` | `pulse:justification` | All | +| `imag:discipline` | `pulse:discipline` | SoftwareSourceCode | +| `imag:repositoryType` | `pulse:repositoryType` | SoftwareSourceCode | +| `imag:relatedToOrganization` | `pulse:relatedToOrganization` | SoftwareSourceCode | +| `md4i:orcid` | `md4i:orcidId` | Person | +| `schema:email` | `pulse:email` | Person, GitAuthor | + +### 4. Added Bidirectional Mappings + +Updated `JSONLD_TO_PYDANTIC_MAPPING` to support both full URIs and prefixed forms: + +```python +# Example: Both forms supported +"http://schema.org/name": "name", +"schema:name": "name", +"https://open-pulse.epfl.ch/ontology#confidence": "confidence", +"pulse:confidence": "confidence", +``` + +### 5. Type Mappings + +Updated to align with PULSE ontology SHACL shapes: + +```python +type_mapping = { + "SoftwareSourceCode": "schema:SoftwareSourceCode", + "Person": "schema:Person", + "Organization": "schema:Organization", + "GitHubOrganization": "schema:GitHubOrganization", + "AcademicCatalogRelation": "pulse:AcademicCatalogRelation", + "CatalogEntity": "pulse:CatalogEntity", + "InfosciencePublication": "schema:ScholarlyArticle", + "Discipline": "pulse:DisciplineEnumeration", + "RepositoryType": "pulse:RepositoryTypeEnumeration", + # ... more +} +``` + +### 6. Documentation + +Created comprehensive documentation: + +#### `docs/PYDANTIC_JSONLD_MAPPING.md` +- Complete property mappings for all models +- SHACL shape references +- Datatype specifications +- Usage examples +- Validation rules + +#### `docs/JSONLD_CONVERSION_SUMMARY.md` +- Quick reference tables +- Common use cases +- Example JSON-LD outputs +- Migration notes from imaging-plaza +- ORCID handling specifics + +## New Features + +### Academic Catalog Relations + +The system now supports linking entities to academic catalogs: + +```python +AcademicCatalogRelation( + catalogType="infoscience", + entityType="person", + entity=CatalogEntity( + uuid="abc-123", + name="Jane Doe", + email="jane@epfl.ch", + profileUrl="https://infoscience.epfl.ch/entities/person/abc-123" + ), + confidence=0.95, + justification="Matched on name and email", + matchedOn=["name", "email"] +) +``` + +This converts to: + +```json +{ + "@type": "pulse:AcademicCatalogRelation", + "pulse:catalogType": "infoscience", + "pulse:entityType": "person", + "pulse:hasCatalogEntity": { + "@type": "pulse:CatalogEntity", + "pulse:uuid": "abc-123", + "schema:name": "Jane Doe", + "pulse:email": "jane@epfl.ch", + "pulse:profileUrl": {"@id": "https://infoscience.epfl.ch/entities/person/abc-123"} + }, + "pulse:confidence": 0.95, + "pulse:justification": "Matched on name and email", + "pulse:matchedOn": ["name", "email"] +} +``` + +### Wikidata Discipline Mapping + +Disciplines are now mapped to Wikidata entities: + +```python +Discipline.BIOLOGY # → wd:Q420 +Discipline.MATHEMATICS # → wd:Q395 +Discipline.PHYSICS # → wd:Q413 +``` + +### PULSE Repository Types + +Repository types use PULSE ontology enumerations: + +```python +RepositoryType.SOFTWARE # → pulse:Software +RepositoryType.EDUCATIONAL_RESOURCE # → pulse:EducationalResource +RepositoryType.DATA # → pulse:Data +``` + +## Validation + +The mappings align with PULSE ontology SHACL shapes: + +### Key Constraints +- `schema:name`: max 60 characters +- `schema:description`: max 2000 characters +- `schema:codeRepository`: pattern `^http.*` +- `pulse:confidence`: range 0.0-1.0 +- `schema:author`: required, Person or Organization +- `pulse:catalogType`: enum (infoscience, orcid, ror, wikidata) +- `pulse:entityType`: enum (person, organization, publication, project) + +## Migration Path + +### Backward Compatibility + +Old properties are still supported in JSON-LD input but will be converted to new properties: + +```python +# Both work: +"imag:confidence" → mapped to "confidence" +"pulse:confidence" → mapped to "confidence" +``` + +### Code Changes Required + +If you're using the old property names in code: + +```python +# Before +"imag:relatedToOrganization" +"imag:infoscienceEntities" + +# After +"pulse:relatedToOrganization" +"pulse:hasAcademicCatalogRelation" +``` + +## Testing + +### Using the CLI Tool (Recommended) + +A command-line tool is available for easy conversion: + +```bash +# Convert JSON to JSON-LD +python scripts/convert_json_jsonld.py to-jsonld input.json output.jsonld \ + --base-url https://github.com/user/repo + +# Convert JSON-LD to JSON +python scripts/convert_json_jsonld.py to-json input.jsonld output.json +``` + +See [JSON-LD Conversion CLI Guide](./JSON_JSONLD_CONVERSION_CLI.md) for detailed usage. + +### Using Python Code + +To test the conversion in Python: + +```python +from src.data_models.repository import SoftwareSourceCode +from src.data_models.models import Person, RepositoryType +from src.data_models.conversion import convert_pydantic_to_jsonld + +repo = SoftwareSourceCode( + name="Test Repo", + description="A test repository", + codeRepository=["https://github.com/test/repo"], + author=[ + Person( + name="Test User", + orcid="0000-0002-1234-5678" + ) + ], + repositoryType=RepositoryType.SOFTWARE, + repositoryTypeJustification=["Contains source code"] +) + +jsonld = convert_pydantic_to_jsonld(repo, base_url="https://github.com/test/repo") +print(jsonld) +``` + +## Files Modified + +1. `src/data_models/conversion.py` - Main conversion logic +2. `docs/PYDANTIC_JSONLD_MAPPING.md` - Complete mapping documentation +3. `docs/JSONLD_CONVERSION_SUMMARY.md` - Quick reference guide + +## Next Steps + +1. **SHACL Validation**: Implement SHACL validation using the PULSE shapes +2. **RDF Export**: Add Turtle/N-Triples serialization options +3. **GraphDB Integration**: Connect to EPFL's triplestore +4. **SPARQL Queries**: Create example queries for common use cases +5. **CLI Tool**: Add command-line tool for JSON→JSON-LD conversion + +## References + +- [PULSE Ontology](https://open-pulse.epfl.ch/ontology#) +- [Schema.org](http://schema.org/) +- [Software Description Ontology](https://w3id.org/okn/o/sd#) +- [Metadata4Ing](http://w3id.org/nfdi4ing/metadata4ing#) +- [Wikidata](https://www.wikidata.org/) + +## Version + +- **Date**: 2025-11-06 +- **Author**: GitHub Copilot +- **Version**: 2.0.0 (PULSE integration) diff --git a/docs/JSON_JSONLD_CONVERSION_CLI.md b/docs/JSON_JSONLD_CONVERSION_CLI.md new file mode 100644 index 0000000..aeb7928 --- /dev/null +++ b/docs/JSON_JSONLD_CONVERSION_CLI.md @@ -0,0 +1,461 @@ +# JSON ↔ JSON-LD Conversion Guide + +## Quick Start + +### Installation + +Make sure you're in the project directory and have the dependencies installed: + +```bash +cd /home/rmfranken/git-metadata-extractor +# If using uv (recommended) +uv sync +# Or with pip +pip install -e . +``` + +### Basic Usage + +#### Convert JSON to JSON-LD + +```bash +python scripts/convert_json_jsonld.py to-jsonld input.json output.jsonld +``` + +**With base URL (recommended):** +```bash +python scripts/convert_json_jsonld.py to-jsonld input.json output.jsonld \ + --base-url https://github.com/your-org/your-repo +``` + +#### Convert JSON-LD to JSON + +```bash +python scripts/convert_json_jsonld.py to-json input.jsonld output.json +``` + +## Detailed Examples + +### Example 1: Convert Repository Metadata to JSON-LD + +**Input file** (`my_repo.json`): +```json +{ + "name": "My Research Software", + "description": "A tool for scientific computing", + "codeRepository": ["https://github.com/example/my-repo"], + "license": "https://spdx.org/licenses/MIT", + "author": [ + { + "type": "Person", + "name": "Jane Doe", + "orcid": "0000-0002-1234-5678", + "affiliations": ["EPFL"] + } + ], + "repositoryType": "software", + "repositoryTypeJustification": ["Contains source code and documentation"], + "discipline": ["Biology", "Computer Engineering"], + "disciplineJustification": ["Computational biology tools", "Software engineering"] +} +``` + +**Command:** +```bash +python scripts/convert_json_jsonld.py to-jsonld my_repo.json my_repo.jsonld \ + --base-url https://github.com/example/my-repo +``` + +**Output** (`my_repo.jsonld`): +```json +{ + "@context": { + "schema": "http://schema.org/", + "sd": "https://w3id.org/okn/o/sd#", + "pulse": "https://open-pulse.epfl.ch/ontology#", + "md4i": "http://w3id.org/nfdi4ing/metadata4ing#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "owl": "http://www.w3.org/2002/07/owl#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "dcterms": "http://purl.org/dc/terms/", + "wd": "http://www.wikidata.org/entity/" + }, + "@graph": [ + { + "@id": "https://github.com/example/my-repo", + "@type": "schema:SoftwareSourceCode", + "schema:name": "My Research Software", + "schema:description": "A tool for scientific computing", + "schema:codeRepository": [ + {"@id": "https://github.com/example/my-repo"} + ], + "schema:license": "https://spdx.org/licenses/MIT", + "schema:author": [ + { + "@type": "schema:Person", + "schema:name": "Jane Doe", + "md4i:orcidId": {"@id": "https://orcid.org/0000-0002-1234-5678"}, + "schema:affiliation": ["EPFL"] + } + ], + "pulse:repositoryType": "pulse:Software", + "pulse:justification": [ + "Contains source code and documentation", + "Computational biology tools", + "Software engineering" + ], + "pulse:discipline": ["Biology", "Computer Engineering"] + } + ] +} +``` + +### Example 2: Convert JSON-LD Back to JSON + +```bash +python scripts/convert_json_jsonld.py to-json my_repo.jsonld my_repo_restored.json +``` + +This will convert the JSON-LD back to the Pydantic JSON format. + +## Using in Python Code + +You can also use the conversion functions directly in Python: + +### Convert to JSON-LD + +```python +from src.data_models.repository import SoftwareSourceCode +from src.data_models.models import Person, RepositoryType +from src.data_models.conversion import convert_pydantic_to_jsonld +import json + +# Create a Pydantic model +repo = SoftwareSourceCode( + name="My Research Software", + description="A tool for scientific computing", + codeRepository=["https://github.com/example/my-repo"], + license="https://spdx.org/licenses/MIT", + author=[ + Person( + name="Jane Doe", + orcid="0000-0002-1234-5678", + affiliations=["EPFL"] + ) + ], + repositoryType=RepositoryType.SOFTWARE, + repositoryTypeJustification=["Contains source code"] +) + +# Convert to JSON-LD +jsonld = convert_pydantic_to_jsonld( + repo, + base_url="https://github.com/example/my-repo" +) + +# Save to file +with open('output.jsonld', 'w') as f: + json.dump(jsonld, f, indent=2) +``` + +### Convert from JSON-LD + +```python +from src.data_models.conversion import convert_jsonld_to_pydantic +import json + +# Load JSON-LD +with open('input.jsonld', 'r') as f: + jsonld_data = json.load(f) + +# Extract graph +graph = jsonld_data.get("@graph", [jsonld_data]) + +# Convert to Pydantic +software = convert_jsonld_to_pydantic(graph) + +# Access properties +print(f"Name: {software.name}") +print(f"Authors: {[a.name for a in software.author]}") + +# Convert back to dict/JSON +data = software.model_dump(exclude_none=True) +``` + +## Working with Existing Files + +### Convert Your Output File + +If you already have an output file from the metadata extractor: + +```bash +python scripts/convert_json_jsonld.py to-jsonld \ + src/files/output_file.json \ + src/files/output_file.jsonld \ + --base-url https://github.com/your-org/your-repo +``` + +### Batch Conversion + +Convert multiple files: + +```bash +# Create a simple bash script +for json_file in data/*.json; do + base_name=$(basename "$json_file" .json) + python scripts/convert_json_jsonld.py to-jsonld \ + "$json_file" \ + "data/${base_name}.jsonld" +done +``` + +Or in Python: + +```python +from pathlib import Path +from src.data_models.conversion import convert_pydantic_to_jsonld +from src.data_models.repository import SoftwareSourceCode +import json + +input_dir = Path("data/json") +output_dir = Path("data/jsonld") +output_dir.mkdir(exist_ok=True) + +for json_file in input_dir.glob("*.json"): + print(f"Converting {json_file.name}...") + + # Load and convert + with open(json_file) as f: + data = json.load(f) + + repo = SoftwareSourceCode(**data) + jsonld = convert_pydantic_to_jsonld(repo) + + # Save + output_file = output_dir / f"{json_file.stem}.jsonld" + with open(output_file, 'w') as f: + json.dump(jsonld, f, indent=2) + + print(f" → {output_file}") +``` + +## Command Reference + +### to-jsonld Command + +Convert Pydantic JSON to JSON-LD format. + +**Syntax:** +```bash +python scripts/convert_json_jsonld.py to-jsonld INPUT OUTPUT [--base-url URL] +``` + +**Arguments:** +- `INPUT`: Path to input JSON file (Pydantic format) +- `OUTPUT`: Path to output JSON-LD file +- `--base-url`: (Optional) Base URL for @id generation (typically the repository URL) + +**Examples:** +```bash +# Basic conversion +python scripts/convert_json_jsonld.py to-jsonld input.json output.jsonld + +# With base URL +python scripts/convert_json_jsonld.py to-jsonld input.json output.jsonld \ + --base-url https://github.com/user/repo + +# Using absolute paths +python scripts/convert_json_jsonld.py to-jsonld \ + /path/to/input.json \ + /path/to/output.jsonld +``` + +### to-json Command + +Convert JSON-LD to Pydantic JSON format. + +**Syntax:** +```bash +python scripts/convert_json_jsonld.py to-json INPUT OUTPUT +``` + +**Arguments:** +- `INPUT`: Path to input JSON-LD file +- `OUTPUT`: Path to output JSON file (Pydantic format) + +**Examples:** +```bash +# Basic conversion +python scripts/convert_json_jsonld.py to-json input.jsonld output.json + +# Using absolute paths +python scripts/convert_json_jsonld.py to-json \ + /path/to/input.jsonld \ + /path/to/output.json +``` + +## Validation + +### Validate JSON-LD Output + +You can validate your JSON-LD output using online tools or libraries: + +**Online Validators:** +- [JSON-LD Playground](https://json-ld.org/playground/) +- [RDF Translator](https://www.easyrdf.org/converter) + +**Using Python:** +```python +from pyld import jsonld +import json + +# Load your JSON-LD +with open('output.jsonld', 'r') as f: + doc = json.load(f) + +# Expand to see full URIs +expanded = jsonld.expand(doc) +print(json.dumps(expanded, indent=2)) + +# Convert to N-Quads (RDF) +nquads = jsonld.to_rdf(doc, {'format': 'application/n-quads'}) +print(nquads) +``` + +### SHACL Validation + +To validate against PULSE ontology SHACL shapes, you'll need a SHACL validator: + +```python +from pyshacl import validate +import json + +# Load your JSON-LD +with open('output.jsonld', 'r') as f: + data_graph = f.read() + +# Load PULSE SHACL shapes (you'll need the shapes file) +with open('pulse_shapes.ttl', 'r') as f: + shacl_graph = f.read() + +# Validate +conforms, results_graph, results_text = validate( + data_graph=data_graph, + data_graph_format='json-ld', + shacl_graph=shacl_graph, + shacl_graph_format='turtle' +) + +print(f"Conforms: {conforms}") +if not conforms: + print(results_text) +``` + +## Troubleshooting + +### Common Issues + +**Issue: "Module not found" error** +```bash +# Solution: Install dependencies +pip install -e . +# Or with uv +uv sync +``` + +**Issue: "No SoftwareSourceCode entity found"** +```bash +# Solution: Check your JSON-LD structure has @type: schema:SoftwareSourceCode +# and a @graph array +``` + +**Issue: "Invalid ORCID format"** +```bash +# Solution: Use format "0000-0002-1234-5678" or "https://orcid.org/0000-0002-1234-5678" +``` + +**Issue: Validation errors** +```bash +# Solution: Check required fields: +# - name (required) +# - description (required) +# - author (required, at least one) +# - repositoryType (required) +# - repositoryTypeJustification (required) +``` + +### Getting Help + +```bash +# Show help message +python scripts/convert_json_jsonld.py --help + +# Show detailed examples +python scripts/convert_json_jsonld.py to-jsonld --help +``` + +## Advanced Usage + +### Custom Context + +If you need to customize the JSON-LD context, modify `src/data_models/conversion.py`: + +```python +# In convert_pydantic_to_jsonld function +context = { + "schema": "http://schema.org/", + "pulse": "https://open-pulse.epfl.ch/ontology#", + # Add your custom prefixes here + "custom": "https://your-domain.com/ontology#", +} +``` + +### Converting Partial Models + +You can convert individual models (Person, Organization, etc.): + +```python +from src.data_models.models import Person +from src.data_models.conversion import convert_pydantic_to_jsonld + +person = Person( + name="Jane Doe", + orcid="0000-0002-1234-5678" +) + +jsonld = convert_pydantic_to_jsonld(person) +``` + +## Integration with API + +To convert API responses: + +```python +from src.api import extract_metadata +from src.data_models.conversion import convert_pydantic_to_jsonld +import json + +# Extract metadata using API +result = extract_metadata( + repo_url="https://github.com/user/repo", + use_cache=True +) + +# Convert to JSON-LD +jsonld = convert_pydantic_to_jsonld( + result['data'], + base_url="https://github.com/user/repo" +) + +# Save +with open('output.jsonld', 'w') as f: + json.dump(jsonld, f, indent=2) +``` + +## See Also + +- [Full Mapping Documentation](./PYDANTIC_JSONLD_MAPPING.md) +- [Quick Reference Guide](./JSONLD_CONVERSION_SUMMARY.md) +- [PULSE Ontology](https://open-pulse.epfl.ch/ontology#) +- [JSON-LD Specification](https://www.w3.org/TR/json-ld11/) diff --git a/docs/PYDANTIC_JSONLD_MAPPING.md b/docs/PYDANTIC_JSONLD_MAPPING.md new file mode 100644 index 0000000..e1f8708 --- /dev/null +++ b/docs/PYDANTIC_JSONLD_MAPPING.md @@ -0,0 +1,416 @@ +# Pydantic to JSON-LD Mapping Documentation + +This document describes the mapping between Pydantic models and JSON-LD representations based on the PULSE ontology. + +## Ontology Namespaces + +The following namespace prefixes are used in the JSON-LD context: + +| Prefix | Namespace URI | Description | +|--------|---------------|-------------| +| `schema` | `http://schema.org/` | Schema.org vocabulary | +| `sd` | `https://w3id.org/okn/o/sd#` | Software Description Ontology | +| `pulse` | `https://open-pulse.epfl.ch/ontology#` | PULSE ontology (EPFL Open Science) | +| `md4i` | `http://w3id.org/nfdi4ing/metadata4ing#` | Metadata4Ing ontology | +| `rdf` | `http://www.w3.org/1999/02/22-rdf-syntax-ns#` | RDF vocabulary | +| `rdfs` | `http://www.w3.org/2000/01/rdf-schema#` | RDF Schema | +| `owl` | `http://www.w3.org/2002/07/owl#` | OWL vocabulary | +| `xsd` | `http://www.w3.org/2001/XMLSchema#` | XML Schema Datatypes | +| `dcterms` | `http://purl.org/dc/terms/` | Dublin Core Terms | +| `wd` | `http://www.wikidata.org/entity/` | Wikidata entities | + +## Core Data Models + +### SoftwareSourceCode + +Main model representing a software repository. + +| Pydantic Field | JSON-LD Property | RDF Type | Description | +|----------------|------------------|----------|-------------| +| `name` | `schema:name` | `xsd:string` | Repository name | +| `description` | `schema:description` | `xsd:string` | Repository description | +| `codeRepository` | `schema:codeRepository` | `xsd:anyURI` | Code repository URL | +| `dateCreated` | `schema:dateCreated` | `xsd:date` | Creation date | +| `datePublished` | `schema:datePublished` | `xsd:date` | Publication date | +| `license` | `schema:license` | `xsd:anyURI` | SPDX license URL | +| `author` | `schema:author` | `schema:Person` or `schema:Organization` | Authors/contributors | +| `url` | `schema:url` | `xsd:anyURI` | Repository homepage | +| `identifier` | `schema:identifier` | `xsd:string` | Unique identifier | +| `programmingLanguage` | `schema:programmingLanguage` | `xsd:string` | Programming languages | +| `citation` | `schema:citation` | `xsd:anyURI` | Citations | +| `isBasedOn` | `schema:isBasedOn` | `xsd:anyURI` | Based on URL | +| `readme` | `sd:readme` | `xsd:anyURI` | README file URL | +| `discipline` | `pulse:discipline` | `pulse:DisciplineEnumeration` | Scientific disciplines | +| `disciplineJustification` | `pulse:justification` | `xsd:string` | Justification for discipline | +| `repositoryType` | `pulse:repositoryType` | `pulse:RepositoryTypeEnumeration` | Repository type | +| `repositoryTypeJustification` | `pulse:justification` | `xsd:string` | Justification for type | +| `relatedToOrganizations` | `pulse:relatedToOrganization` | `xsd:string` | Related organizations | +| `relatedToOrganizationJustification` | `pulse:justification` | `xsd:string` | Justification for org relation | +| `relatedToEPFL` | `pulse:relatedToEPFL` | `xsd:boolean` | Whether related to EPFL | +| `relatedToEPFLConfidence` | `pulse:confidence` | `xsd:decimal` | Confidence score (0.0-1.0) | +| `relatedToEPFLJustification` | `pulse:justification` | `xsd:string` | Justification for EPFL relation | +| `gitAuthors` | `pulse:gitAuthors` | `schema:Person` | Git commit authors | +| `academicCatalogRelations` | `pulse:hasAcademicCatalogRelation` | `pulse:AcademicCatalogRelation` | Academic catalog relations | +| `applicationCategory` | `schema:applicationCategory` | `xsd:string` | Application categories | +| `featureList` | `schema:featureList` | `xsd:string` | Feature list | +| `image` | `schema:image` | `schema:ImageObject` | Images | +| `isAccessibleForFree` | `schema:isAccessibleForFree` | `xsd:boolean` | Free access | +| `operatingSystem` | `schema:operatingSystem` | `xsd:string` | Operating systems | +| `softwareRequirements` | `schema:softwareRequirements` | `xsd:string` | Software requirements | +| `processorRequirements` | `schema:processorRequirements` | `xsd:string` | Processor requirements | +| `memoryRequirements` | `schema:memoryRequirements` | `xsd:integer` | Memory requirements | +| `requiresGPU` | `pulse:requiresGPU` | `xsd:boolean` | GPU requirements | +| `supportingData` | `schema:supportingData` | `schema:DataFeed` | Supporting data | +| `conditionsOfAccess` | `schema:conditionsOfAccess` | `xsd:string` | Access conditions | +| `hasAcknowledgements` | `sd:hasAcknowledgements` | `xsd:string` | Acknowledgements | +| `hasDocumentation` | `sd:hasDocumentation` | `xsd:anyURI` | Documentation URL | +| `hasExecutableInstructions` | `sd:hasExecutableInstructions` | `xsd:string` | Executable instructions | +| `hasExecutableNotebook` | `pulse:hasExecutableNotebook` | `schema:SoftwareApplication` | Executable notebooks | +| `hasFunding` | `sd:hasFunding` | `schema:Grant` | Funding information | +| `hasSoftwareImage` | `sd:hasSoftwareImage` | `schema:SoftwareApplication` | Software images | +| `imagingModality` | `pulse:imagingModality` | `xsd:string` | Imaging modalities | +| `isPluginModuleOf` | `pulse:isPluginModuleOf` | `xsd:string` | Plugin module of | +| `relatedDatasets` | `pulse:relatedDatasets` | `xsd:string` | Related datasets | +| `relatedPublications` | `pulse:relatedPublications` | `xsd:string` | Related publications | +| `relatedModels` | `pulse:relatedModels` | `xsd:string` | Related models | +| `relatedAPIs` | `pulse:relatedAPIs` | `xsd:string` | Related APIs | + +**JSON-LD Type**: `schema:SoftwareSourceCode` + +### Person + +Represents an individual author or contributor. + +| Pydantic Field | JSON-LD Property | RDF Type | Description | +|----------------|------------------|----------|-------------| +| `type` | `@type` | - | Type discriminator ("Person") | +| `name` | `schema:name` | `xsd:string` | Person's full name | +| `email` | `pulse:email` | `xsd:string` | Email address(es) | +| `orcid` | `md4i:orcidId` | `xsd:string` | ORCID identifier | +| `gitAuthorIds` | `pulse:gitAuthorIds` | `xsd:string` | Git author identifiers | +| `affiliations` | `schema:affiliation` | `xsd:string` | All affiliations | +| `currentAffiliation` | `schema:affiliation` | `xsd:string` | Current affiliation | +| `affiliationHistory` | `pulse:affiliationHistory` | - | Temporal affiliation data | +| `contributionSummary` | `pulse:contributionSummary` | `xsd:string` | Contribution summary | +| `biography` | `schema:description` | `xsd:string` | Biographical information | +| `academicCatalogRelations` | `pulse:hasAcademicCatalogRelation` | `pulse:AcademicCatalogRelation` | Academic catalog relations | + +**JSON-LD Type**: `schema:Person` + +**SHACL Shape**: Defined in PULSE ontology as `schema:Person` with properties: +- `schema:name` (required) +- `md4i:orcidId` (optional) +- `schema:affiliation` (optional) +- `pulse:username` (optional) + +### Organization + +Represents an institution, lab, or company. + +| Pydantic Field | JSON-LD Property | RDF Type | Description | +|----------------|------------------|----------|-------------| +| `type` | `@type` | - | Type discriminator ("Organization") | +| `legalName` | `schema:legalName` | `xsd:string` | Legal/official name | +| `hasRorId` | `md4i:hasRorId` | `xsd:anyURI` | ROR identifier URL | +| `alternateNames` | `schema:alternateName` | `xsd:string` | Alternative names | +| `organizationType` | `schema:additionalType` | `xsd:string` | Organization type | +| `parentOrganization` | `schema:parentOrganization` | `xsd:string` | Parent organization | +| `country` | `schema:addressCountry` | `xsd:string` | Country | +| `website` | `schema:url` | `xsd:anyURI` | Website URL | +| `attributionConfidence` | `pulse:confidence` | `xsd:decimal` | Attribution confidence | +| `academicCatalogRelations` | `pulse:hasAcademicCatalogRelation` | `pulse:AcademicCatalogRelation` | Academic catalog relations | + +**JSON-LD Type**: `schema:Organization` + +**SHACL Shape**: Defined in PULSE ontology as `schema:Organization` with properties: +- `schema:legalName` (required) +- `md4i:hasRorId` (optional) + +### GitHubOrganization + +Represents a GitHub organization with enriched metadata. + +| Pydantic Field | JSON-LD Property | RDF Type | Description | +|----------------|------------------|----------|-------------| +| `name` | `schema:name` | `xsd:string` | Organization name | +| `organizationType` | `schema:additionalType` | `xsd:string` | Organization type | +| `description` | `schema:description` | `xsd:string` | Description | +| `discipline` | `pulse:discipline` | `pulse:DisciplineEnumeration` | Disciplines | +| `disciplineJustification` | `pulse:justification` | `xsd:string` | Discipline justification | +| `relatedToEPFL` | `pulse:relatedToEPFL` | `xsd:boolean` | EPFL relation | +| `relatedToEPFLJustification` | `pulse:justification` | `xsd:string` | EPFL relation justification | +| `relatedToEPFLConfidence` | `pulse:confidence` | `xsd:decimal` | Confidence score | +| `academicCatalogRelations` | `pulse:hasAcademicCatalogRelation` | `pulse:AcademicCatalogRelation` | Academic catalog relations | +| `githubOrganizationMetadata` | `pulse:metadata` | - | GitHub metadata | + +**JSON-LD Type**: `schema:GitHubOrganization` + +**SHACL Shape**: Defined in PULSE ontology with properties: +- `pulse:username` (GitHub login) +- `pulse:hasRepository` (repositories) +- `schema:affiliation` (affiliations) + +## Academic Catalog Models + +### AcademicCatalogRelation + +Represents a relationship to an entity in an academic catalog (Infoscience, ORCID, ROR, etc.). + +| Pydantic Field | JSON-LD Property | RDF Type | Description | +|----------------|------------------|----------|-------------| +| `catalogType` | `pulse:catalogType` | `xsd:string` | Catalog type (infoscience, orcid, ror, wikidata) | +| `entityType` | `pulse:entityType` | `xsd:string` | Entity type (person, organization, publication, project) | +| `entity` | `pulse:hasCatalogEntity` | `pulse:CatalogEntity` | The catalog entity | +| `confidence` | `pulse:confidence` | `xsd:decimal` | Confidence score (0.0-1.0) | +| `justification` | `pulse:justification` | `xsd:string` | Justification text | +| `matchedOn` | `pulse:matchedOn` | `xsd:string` | Fields matched on | + +**JSON-LD Type**: `pulse:AcademicCatalogRelation` + +**SHACL Shape**: Defined in PULSE ontology with constraints: +- `pulse:catalogType` (required, enum: infoscience, orcid, ror, wikidata) +- `pulse:entityType` (required, enum: person, organization, publication, project) +- `pulse:hasCatalogEntity` (required) +- `pulse:confidence` (required, range: 0.0-1.0) +- `pulse:justification` (required) + +### CatalogEntity + +Represents an entity from an academic catalog. + +| Pydantic Field | JSON-LD Property | RDF Type | Description | +|----------------|------------------|----------|-------------| +| `uuid` | `pulse:uuid` | `xsd:string` | Unique identifier | +| `name` | `schema:name` | `xsd:string` | Entity name | +| `email` | `pulse:email` | `xsd:string` | Email address | +| `orcid` | `md4i:orcidId` | `xsd:string` | ORCID identifier | +| `affiliation` | `schema:affiliation` | `xsd:string` | Affiliation | +| `profileUrl` | `pulse:profileUrl` | `xsd:anyURI` | Profile URL | + +**JSON-LD Type**: `pulse:CatalogEntity` + +**SHACL Shape**: Defined in PULSE ontology with properties: +- `pulse:uuid` (required) +- `schema:name` (required) +- `pulse:email` (optional) +- `md4i:orcidId` (optional) +- `schema:affiliation` (optional) +- `pulse:profileUrl` (optional) + +### InfosciencePublication + +Publication from EPFL's Infoscience repository. + +| Pydantic Field | JSON-LD Property | RDF Type | Description | +|----------------|------------------|----------|-------------| +| `type` | `@type` | - | Type discriminator | +| `uuid` | `pulse:uuid` | `xsd:string` | DSpace UUID | +| `title` | `schema:name` | `xsd:string` | Publication title | +| `authors` | `schema:author` | `xsd:string` | Author names | +| `abstract` | `schema:abstract` | `xsd:string` | Abstract text | +| `doi` | `schema:identifier` | `xsd:string` | DOI | +| `publication_date` | `schema:datePublished` | `xsd:date` | Publication date | +| `publication_type` | `schema:additionalType` | `xsd:string` | Publication type | +| `url` | `schema:url` | `xsd:anyURI` | Infoscience URL | +| `repository_url` | `schema:codeRepository` | `xsd:anyURI` | Code repository | +| `lab` | `schema:affiliation` | `xsd:string` | Laboratory | +| `subjects` | `schema:keywords` | `xsd:string` | Subject keywords | + +**JSON-LD Type**: `schema:ScholarlyArticle` + +### InfoscienceAuthor + +Author/researcher from Infoscience. + +| Pydantic Field | JSON-LD Property | RDF Type | Description | +|----------------|------------------|----------|-------------| +| `type` | `@type` | - | Type discriminator | +| `uuid` | `pulse:uuid` | `xsd:string` | DSpace UUID | +| `name` | `schema:name` | `xsd:string` | Full name | +| `email` | `pulse:email` | `xsd:string` | Email | +| `orcid` | `md4i:orcidId` | `xsd:string` | ORCID | +| `affiliation` | `schema:affiliation` | `xsd:string` | Affiliation | +| `profile_url` | `pulse:profileUrl` | `xsd:anyURI` | Infoscience profile | + +**JSON-LD Type**: `schema:Person` + +### InfoscienceLab + +Laboratory or organizational unit from Infoscience. + +| Pydantic Field | JSON-LD Property | RDF Type | Description | +|----------------|------------------|----------|-------------| +| `type` | `@type` | - | Type discriminator | +| `uuid` | `pulse:uuid` | `xsd:string` | DSpace UUID | +| `name` | `schema:name` | `xsd:string` | Lab name | +| `description` | `schema:description` | `xsd:string` | Description | +| `url` | `schema:url` | `xsd:anyURI` | Infoscience URL | +| `parent_organization` | `schema:parentOrganization` | `xsd:string` | Parent org | +| `website` | `schema:url` | `xsd:anyURI` | External website | +| `research_areas` | `schema:knowsAbout` | `xsd:string` | Research areas | + +**JSON-LD Type**: `schema:Organization` + +## Supporting Models + +### GitAuthor + +Git commit author information. + +| Pydantic Field | JSON-LD Property | RDF Type | Description | +|----------------|------------------|----------|-------------| +| `name` | `schema:name` | `xsd:string` | Author name | +| `email` | `pulse:email` | `xsd:string` | Email | +| `commits` | `pulse:commits` | `pulse:Commits` | Commit statistics | + +**JSON-LD Type**: `schema:Person` + +### Commits + +Commit statistics. + +| Pydantic Field | JSON-LD Property | RDF Type | Description | +|----------------|------------------|----------|-------------| +| `total` | `pulse:totalCommits` | `xsd:integer` | Total commits | +| `firstCommitDate` | `pulse:firstCommitDate` | `xsd:date` | First commit date | +| `lastCommitDate` | `pulse:lastCommitDate` | `xsd:date` | Last commit date | + +### FundingInformation + +| Pydantic Field | JSON-LD Property | RDF Type | Description | +|----------------|------------------|----------|-------------| +| `identifier` | `schema:identifier` | `xsd:string` | Grant identifier | +| `fundingGrant` | `sd:fundingGrant` | `xsd:string` | Grant number | +| `fundingSource` | `sd:fundingSource` | `schema:Organization` | Funding organization | + +**JSON-LD Type**: `schema:Grant` + +### DataFeed + +| Pydantic Field | JSON-LD Property | RDF Type | Description | +|----------------|------------------|----------|-------------| +| `name` | `schema:name` | `xsd:string` | Name | +| `description` | `schema:description` | `xsd:string` | Description | +| `contentUrl` | `schema:contentUrl` | `xsd:anyURI` | Content URL | +| `measurementTechnique` | `schema:measurementTechnique` | `xsd:string` | Measurement technique | +| `variableMeasured` | `schema:variableMeasured` | `xsd:string` | Variable measured | + +**JSON-LD Type**: `schema:DataFeed` + +### Image + +| Pydantic Field | JSON-LD Property | RDF Type | Description | +|----------------|------------------|----------|-------------| +| `contentUrl` | `schema:contentUrl` | `xsd:anyURI` | Image URL | +| `keywords` | `schema:keywords` | `xsd:string` | Keywords | + +**JSON-LD Type**: `schema:ImageObject` + +## Enumerations + +### Discipline + +Scientific disciplines aligned with Wikidata entities. + +**JSON-LD Type**: `pulse:DisciplineEnumeration` + +**Values**: Mapped to Wikidata entities (e.g., `wd:Q420` for Biology, `wd:Q395` for Mathematics) + +Examples: +- `BIOLOGY` → `wd:Q420` +- `MATHEMATICS` → `wd:Q395` +- `PHYSICS` → `wd:Q413` +- `COMPUTER_ENGINEERING` → `wd:Q428691` + +### RepositoryType + +Repository classification. + +**JSON-LD Type**: `pulse:RepositoryTypeEnumeration` + +**Values**: +- `SOFTWARE` → `pulse:Software` +- `EDUCATIONAL_RESOURCE` → `pulse:EducationalResource` +- `DOCUMENTATION` → `pulse:Documentation` +- `DATA` → `pulse:Data` +- `OTHER` → `pulse:Other` + +## Usage Examples + +### Converting Pydantic to JSON-LD + +```python +from src.data_models.repository import SoftwareSourceCode +from src.data_models.conversion import convert_pydantic_to_jsonld + +# Create a Pydantic model instance +repo = SoftwareSourceCode( + name="My Research Software", + description="A tool for scientific computing", + codeRepository=["https://github.com/example/repo"], + license="https://spdx.org/licenses/MIT", + author=[ + Person( + name="Jane Doe", + orcid="0000-0002-1234-5678", + affiliation=["EPFL"] + ) + ], + repositoryType=RepositoryType.SOFTWARE, + repositoryTypeJustification=["Contains source code and documentation"] +) + +# Convert to JSON-LD +jsonld = convert_pydantic_to_jsonld(repo, base_url="https://github.com/example/repo") +``` + +### Converting JSON-LD to Pydantic + +```python +from src.data_models.conversion import convert_jsonld_to_pydantic + +jsonld_graph = [ + { + "@id": "https://github.com/example/repo", + "@type": "schema:SoftwareSourceCode", + "schema:name": "My Research Software", + "schema:description": "A tool for scientific computing", + # ... more properties + } +] + +repo = convert_jsonld_to_pydantic(jsonld_graph) +``` + +## SHACL Validation + +The PULSE ontology includes SHACL shapes for validation. Key constraints: + +### schema:SoftwareSourceCode +- `schema:name` (required, max 60 chars) +- `schema:description` (required, max 2000 chars) +- `schema:codeRepository` (required, pattern: `^http.*`) +- `schema:dateCreated` (required, datatype: xsd:date) +- `schema:license` (required, pattern: `.*spdx\.org.*`) +- `schema:author` (required, Person or Organization) +- `pulse:discipline` (class: pulse:DisciplineEnumeration) +- `pulse:repositoryType` (class: pulse:RepositoryTypeEnumeration) + +### pulse:AcademicCatalogRelation +- All fields required except `matchedOn` +- `confidence` must be between 0.0 and 1.0 +- `catalogType` must be one of: infoscience, orcid, ror, wikidata +- `entityType` must be one of: person, organization, publication, project + +## References + +- PULSE Ontology: `https://open-pulse.epfl.ch/ontology#` +- Schema.org: `http://schema.org/` +- Software Description Ontology: `https://w3id.org/okn/o/sd#` +- Metadata4Ing: `http://w3id.org/nfdi4ing/metadata4ing#` +- Wikidata: `http://www.wikidata.org/entity/` + +## Version History + +- **2025-11-06**: Updated to align with PULSE ontology, added academic catalog relations +- **Previous**: Based on imaging-plaza ontology diff --git a/justfile b/justfile index c3f0901..be9461a 100644 --- a/justfile +++ b/justfile @@ -102,19 +102,19 @@ docker-up: docker-build docker-run # Run all tests test: - pytest src/test/ -v + PYTHONPATH=src pytest tests/ -v # Run tests with coverage test-coverage: - pytest src/test/ --cov=src --cov-report=html --cov-report=term + PYTHONPATH=src pytest tests/ --cov=src --cov-report=html --cov-report=term # Run specific test file test-file FILE: - pytest {{FILE}} -v + PYTHONPATH=src pytest {{FILE}} -v # Run tests in watch mode (requires pytest-watch) test-watch: - ptw src/test/ + PYTHONPATH=src ptw tests/ # ============================================================================ # Cache Management (via API) diff --git a/pyproject.toml b/pyproject.toml index 4dabd09..ed7f38c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "git-metadata-extractor" version = "2.0.0" description = "This project is designed to classify imaging software repositories and extract relevant information using AI models." readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" license = { text = "MIT" } authors = [ { name = "Quentin Chappuis", email = "quentin.chappuis@epfl.ch" }, @@ -16,7 +16,6 @@ authors = [ ] classifiers = [ "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "License :: OSI Approved :: MIT License", diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..00f6ac7 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,132 @@ +# Conversion Scripts + +This directory contains utility scripts for working with the git-metadata-extractor. + +## Available Scripts + +### convert_json_jsonld.py + +Command-line tool for converting between JSON and JSON-LD formats. + +**Quick Start:** + +```bash +# Convert JSON to JSON-LD +python scripts/convert_json_jsonld.py to-jsonld input.json output.jsonld + +# Convert JSON-LD to JSON +python scripts/convert_json_jsonld.py to-json input.jsonld output.json +``` + +**Documentation:** See [JSON-LD Conversion CLI Guide](../docs/JSON_JSONLD_CONVERSION_CLI.md) + +## Usage Examples + +### Example 1: Convert Repository Metadata + +```bash +python scripts/convert_json_jsonld.py to-jsonld \ + src/files/output_file.json \ + src/files/output_file.jsonld \ + --base-url https://github.com/your-org/your-repo +``` + +### Example 2: Round-trip Conversion + +```bash +# Original → JSON-LD +python scripts/convert_json_jsonld.py to-jsonld data.json data.jsonld + +# JSON-LD → Back to JSON +python scripts/convert_json_jsonld.py to-json data.jsonld data_restored.json +``` + +### Example 3: Batch Processing + +```bash +# Convert all JSON files in a directory +for json_file in data/*.json; do + base_name=$(basename "$json_file" .json) + python scripts/convert_json_jsonld.py to-jsonld \ + "$json_file" \ + "output/${base_name}.jsonld" +done +``` + +### Example 4: Convert with Auto-detected Base URLs + +```bash +# Regenerate JSON-LD files with proper GitHub URLs from source JSON +rm -rf data/1_batch_11122025/1_batch/converted/* + +for file in data/1_batch_11122025/1_batch/entities-properties/*.json; do + if [ -f "$file" ]; then + base=$(basename "${file%.json}") + link=$(.venv/bin/python -c "import json; print(json.load(open('$file')).get('link', ''))" 2>/dev/null) + + if [ -n "$link" ]; then + .venv/bin/python scripts/convert_json_jsonld.py to-jsonld "$file" "data/1_batch_11122025/1_batch/converted/${base}.jsonld" --base-url "$link" + else + .venv/bin/python scripts/convert_json_jsonld.py to-jsonld "$file" "data/1_batch_11122025/1_batch/converted/${base}.jsonld" + fi + echo "Converted $base" + fi +done +``` + +## Uploading to Tentris + +### upload_all_to_tentris.sh + +Batch upload script for uploading JSON-LD files to a Tentris triplestore. + +**Setup:** + +```bash +# Make the script executable +chmod +x scripts/upload_all_to_tentris.sh +``` + +**Usage:** + +```bash +# Run the batch upload +./scripts/upload_all_to_tentris.sh +``` + +The script will: +1. Authenticate with Tentris +2. Convert each JSON-LD file to Turtle format +3. Upload to the Tentris graph store +4. Show progress and summary + +**Clear the default graph before uploading:** + +```bash +# Login to Tentris +curl -c "/tmp/tentris-cookie" \ + --data "username=YOUR_USERNAME&password=YOUR_PASSWORD" \ + http://YOUR_TENTRIS_HOST:PORT/login + +# Clear the default graph +curl -b "/tmp/tentris-cookie" \ + -H "Content-Type: application/sparql-update" \ + --data "CLEAR DEFAULT" \ + http://YOUR_TENTRIS_HOST:PORT/update +``` + +## Requirements + +Make sure you have the project dependencies installed: + +```bash +pip install -e . +# Or with uv +uv sync +``` + +## See Also + +- [Pydantic↔JSON-LD Mapping Documentation](../docs/PYDANTIC_JSONLD_MAPPING.md) +- [Quick Reference Guide](../docs/JSONLD_CONVERSION_SUMMARY.md) +- [Detailed CLI Guide](../docs/JSON_JSONLD_CONVERSION_CLI.md) diff --git a/scripts/convert_json_jsonld.py b/scripts/convert_json_jsonld.py new file mode 100755 index 0000000..faa5f5e --- /dev/null +++ b/scripts/convert_json_jsonld.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 +""" +CLI tool for converting between JSON and JSON-LD formats. + +Usage: + # Convert Pydantic JSON to JSON-LD + python scripts/convert_json_jsonld.py to-jsonld input.json output.jsonld + + # Convert JSON-LD to Pydantic JSON + python scripts/convert_json_jsonld.py to-json input.jsonld output.json + + # With custom base URL + python scripts/convert_json_jsonld.py to-jsonld input.json output.jsonld --base-url https://github.com/user/repo +""" + +import argparse +import json +import sys +from pathlib import Path +from typing import Optional + +# Add src to path +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +from data_models.conversion import ( + convert_jsonld_to_pydantic, + convert_pydantic_to_jsonld, +) +from data_models.organization import GitHubOrganization +from data_models.repository import SoftwareSourceCode +from data_models.user import GitHubUser + + +def convert_to_jsonld( + input_file: Path, + output_file: Path, + base_url: Optional[str] = None, +): + """Convert Pydantic JSON to JSON-LD format.""" + print(f"📖 Reading Pydantic JSON from: {input_file}") + + with open(input_file, encoding="utf-8") as f: + data = json.load(f) + + print("🔄 Converting to Pydantic model...") + + # Detect type and validate + model_obj = None + model_type = None + + # Check if it's from the API output format (has "output" wrapper) + if "output" in data and "type" in data: + api_type = data.get("type") + inner_data = data.get("output", {}) + + if api_type == "organization": + try: + model_obj = GitHubOrganization(**inner_data) + model_type = "GitHubOrganization" + print("✅ Successfully validated as GitHubOrganization") + except Exception as e: + print(f"❌ Error validating as GitHubOrganization: {e}") + sys.exit(1) + elif api_type == "user": + try: + model_obj = GitHubUser(**inner_data) + model_type = "GitHubUser" + print("✅ Successfully validated as GitHubUser") + except Exception as e: + print(f"❌ Error validating as GitHubUser: {e}") + sys.exit(1) + elif api_type == "repository": + try: + model_obj = SoftwareSourceCode(**inner_data) + model_type = "SoftwareSourceCode" + print("✅ Successfully validated as SoftwareSourceCode") + except Exception as e: + print(f"❌ Error validating as SoftwareSourceCode: {e}") + sys.exit(1) + else: + print(f"❌ Unknown API type: {api_type}") + sys.exit(1) + else: + # Try to detect model type from data structure + # Try SoftwareSourceCode first (has repositoryType) + if "repositoryType" in data or "codeRepository" in data: + try: + model_obj = SoftwareSourceCode(**data) + model_type = "SoftwareSourceCode" + print("✅ Successfully validated as SoftwareSourceCode") + except Exception as e: + print(f"❌ Error validating as SoftwareSourceCode: {e}") + sys.exit(1) + # Try GitHubOrganization (has organizationType or githubOrganizationMetadata) + elif "organizationType" in data or "githubOrganizationMetadata" in data: + try: + model_obj = GitHubOrganization(**data) + model_type = "GitHubOrganization" + print("✅ Successfully validated as GitHubOrganization") + except Exception as e: + print(f"❌ Error validating as GitHubOrganization: {e}") + sys.exit(1) + # Try GitHubUser (has githubHandle or githubUserMetadata) + elif "githubHandle" in data or "githubUserMetadata" in data: + try: + model_obj = GitHubUser(**data) + model_type = "GitHubUser" + print("✅ Successfully validated as GitHubUser") + except Exception as e: + print(f"❌ Error validating as GitHubUser: {e}") + sys.exit(1) + else: + print( + "❌ Could not detect model type. Expected SoftwareSourceCode, GitHubOrganization, or GitHubUser", + ) + sys.exit(1) + + print("🔄 Converting to JSON-LD...") + + # Convert to JSON-LD + jsonld = convert_pydantic_to_jsonld(model_obj, base_url=base_url) + + print(f"💾 Writing JSON-LD to: {output_file}") + + with open(output_file, "w", encoding="utf-8") as f: + json.dump(jsonld, f, indent=2, ensure_ascii=False) + + print("✅ Conversion complete!") + print("\n📊 Summary:") + print(f" - Type: {model_type}") + print(f" - Input: {input_file} ({input_file.stat().st_size:,} bytes)") + print(f" - Output: {output_file} ({output_file.stat().st_size:,} bytes)") + if base_url: + print(f" - Base URL: {base_url}") + + +def convert_to_json(input_file: Path, output_file: Path): + """Convert JSON-LD to Pydantic JSON format.""" + print(f"📖 Reading JSON-LD from: {input_file}") + + with open(input_file, encoding="utf-8") as f: + jsonld_data = json.load(f) + + print("🔄 Converting to Pydantic model...") + + # Extract graph if present + graph = jsonld_data.get("@graph", [jsonld_data]) + + # Detect the type from @type in the graph + model_obj = None + model_type = None + + if isinstance(graph, list) and len(graph) > 0: + first_entity = graph[0] + entity_type = first_entity.get("@type", "") + + # Detect based on @type + if entity_type in ["schema:SoftwareSourceCode"]: + # Try to convert as repository + try: + model_obj = convert_jsonld_to_pydantic(graph) + if model_obj: + model_type = "SoftwareSourceCode" + print("✅ Successfully converted to SoftwareSourceCode") + except Exception as e: + print(f"❌ Error converting as SoftwareSourceCode: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) + elif entity_type in ["schema:GitHubOrganization", "schema:Organization"]: + # For now, organizations need manual reconstruction since convert_jsonld_to_pydantic + # only handles SoftwareSourceCode. Create GitHubOrganization from the data + print("⚠️ Note: Organization conversion from JSON-LD is simplified") + try: + # Extract basic fields - this is a simplified conversion + org_data = { + "name": first_entity.get("schema:name"), + "organizationType": first_entity.get("schema:additionalType"), + "description": first_entity.get("schema:description"), + } + # TODO: Implement full reverse mapping for organizations + model_obj = org_data # Return as dict for now + model_type = "GitHubOrganization" + print("✅ Extracted organization data (simplified)") + except Exception as e: + print(f"❌ Error converting organization: {e}") + sys.exit(1) + elif entity_type in ["schema:Person"]: + # Check if it's a GitHubUser (has username) or generic Person + if "schema:username" in first_entity or "pulse:metadata" in first_entity: + print("⚠️ Note: User conversion from JSON-LD is simplified") + try: + user_data = { + "name": first_entity.get("schema:name"), + "githubHandle": first_entity.get("schema:username"), + } + # TODO: Implement full reverse mapping for users + model_obj = user_data # Return as dict for now + model_type = "GitHubUser" + print("✅ Extracted user data (simplified)") + except Exception as e: + print(f"❌ Error converting user: {e}") + sys.exit(1) + else: + print("❌ Generic Person type not yet supported for reverse conversion") + sys.exit(1) + else: + print(f"❌ Unknown entity type: {entity_type}") + sys.exit(1) + else: + print("❌ No entities found in JSON-LD graph") + sys.exit(1) + + if model_obj is None: + print("❌ Error: Could not convert JSON-LD") + sys.exit(1) + + print("🔄 Serializing to JSON...") + + # Convert to dict + if hasattr(model_obj, "model_dump"): + data = model_obj.model_dump(exclude_none=True, exclude_unset=True) + else: + data = model_obj # Already a dict + + print(f"💾 Writing JSON to: {output_file}") + + with open(output_file, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False, default=str) + + print("✅ Conversion complete!") + print("\n📊 Summary:") + print(f" - Type: {model_type}") + print(f" - Input: {input_file} ({input_file.stat().st_size:,} bytes)") + print(f" - Output: {output_file} ({output_file.stat().st_size:,} bytes)") + + +def main(): + parser = argparse.ArgumentParser( + description="Convert between JSON and JSON-LD formats", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Convert Pydantic JSON to JSON-LD + python scripts/convert_json_jsonld.py to-jsonld input.json output.jsonld + + # Convert JSON-LD to Pydantic JSON + python scripts/convert_json_jsonld.py to-json input.jsonld output.json + + # With custom base URL + python scripts/convert_json_jsonld.py to-jsonld input.json output.jsonld \\ + --base-url https://github.com/user/repo + """, + ) + + parser.add_argument( + "command", + choices=["to-jsonld", "to-json"], + help="Conversion direction", + ) + + parser.add_argument( + "input", + type=Path, + help="Input file path", + ) + + parser.add_argument( + "output", + type=Path, + help="Output file path", + ) + + parser.add_argument( + "--base-url", + type=str, + help="Base URL for @id generation (only for to-jsonld)", + ) + + args = parser.parse_args() + + # Check input file exists + if not args.input.exists(): + print(f"❌ Error: Input file not found: {args.input}") + sys.exit(1) + + # Create output directory if needed + args.output.parent.mkdir(parents=True, exist_ok=True) + + # Run conversion + try: + if args.command == "to-jsonld": + convert_to_jsonld(args.input, args.output, args.base_url) + else: # to-json + if args.base_url: + print("⚠️ Warning: --base-url is ignored for to-json conversion") + convert_to_json(args.input, args.output) + except Exception as e: + print(f"\n❌ Conversion failed: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/test_tentris_upload.sh b/scripts/test_tentris_upload.sh new file mode 100755 index 0000000..5cf463d --- /dev/null +++ b/scripts/test_tentris_upload.sh @@ -0,0 +1,132 @@ +#!/bin/bash + +# Configuration +TENTRIS_HOST="http://128.178.219.51:7502" +USERNAME="admin" # !!! CHANGE THIS !!! +PASSWORD="shrekislife" # !!! CHANGE THIS !!! +COOKIE_FILE="/tmp/tentris-cookie" +TEST_FILE="/home/rmfranken/git-metadata-extractor/data/1_batch_11122025/1_batch/converted/0xKDI.jsonld" + +echo "=== Tentris Authentication & Upload Test ===" +echo "" + +# Step 1: Try multiple login methods +echo "Step 1: Logging in to Tentris..." +echo "Using username: $USERNAME" + +# Try the login method from the documentation +# Just use --data (defaults to POST with proper content-type) +echo " Trying form-based login..." +login_response=$(curl -s -w "\n%{http_code}" -c "$COOKIE_FILE" \ + --data "username=$USERNAME&password=$PASSWORD" \ + "$TENTRIS_HOST/login") + +login_code=$(echo "$login_response" | tail -n1) +echo "Login response code: $login_code" + +# 303 is a redirect (See Other) which usually means success for POST to login +if [[ "$login_code" =~ ^(2[0-9][0-9]|303)$ ]]; then + echo "✅ Login successful (got redirect or 2xx)" + echo "Cookie saved to: $COOKIE_FILE" + + # Show what's in the cookie file + if [ -f "$COOKIE_FILE" ]; then + echo "Cookie contents:" + cat "$COOKIE_FILE" + fi +else + echo "❌ Login failed (HTTP $login_code)" + echo "Response: $(echo "$login_response" | head -n -1)" + exit 1 +fi + +echo "" + +# Step 2: Convert JSON-LD to Turtle format +echo "Step 2: Converting JSON-LD to Turtle format..." +echo "Input file: $TEST_FILE" + +TEMP_TURTLE="/tmp/tentris_upload.ttl" + +# Use Python with rdflib to convert JSON-LD to Turtle +# Use the venv python if available, otherwise fall back to python3 +PYTHON_CMD="/home/rmfranken/git-metadata-extractor/.venv/bin/python" +if [ ! -f "$PYTHON_CMD" ]; then + PYTHON_CMD="python3" +fi + +$PYTHON_CMD << 'PYEOF' +import sys +import json +from rdflib import Graph + +try: + # Load JSON-LD file + g = Graph() + g.parse("/home/rmfranken/git-metadata-extractor/data/1_batch_11122025/1_batch/converted/0xKDI.jsonld", format="json-ld") + + # Serialize to Turtle + with open("/tmp/tentris_upload.ttl", "w", encoding="utf-8") as f: + f.write(g.serialize(format="turtle")) + + print(f"✅ Converted to Turtle ({len(g)} triples)") + sys.exit(0) +except Exception as e: + print(f"❌ Conversion failed: {e}") + sys.exit(1) +PYEOF + +if [ $? -ne 0 ]; then + echo "Failed to convert JSON-LD to Turtle" + exit 1 +fi + +echo "" + +# Step 3: Upload the Turtle file +echo "Step 3: Uploading Turtle file to Tentris..." +echo "File: $TEMP_TURTLE" + +upload_response=$(curl -s -w "\n%{http_code}" -b "$COOKIE_FILE" \ + -X POST \ + -H "Content-Type: text/turtle" \ + --data-binary "@$TEMP_TURTLE" \ + "$TENTRIS_HOST/graph-store?default") + +upload_code=$(echo "$upload_response" | tail -n1) +echo "Upload response code: $upload_code" + +if [[ "$upload_code" =~ ^2[0-9][0-9]$ ]]; then + echo "✅ Upload successful" + echo "Response: $(echo "$upload_response" | head -n -1)" +else + echo "❌ Upload failed (HTTP $upload_code)" + echo "Response: $(echo "$upload_response" | head -n -1)" + exit 1 +fi + +echo "" + +# Step 4: Verify with a SPARQL query +echo "Step 4: Verifying upload with SPARQL query..." +query_response=$(curl -s -w "\n%{http_code}" -b "$COOKIE_FILE" \ + -H "Content-Type: application/sparql-query" \ + --data "SELECT (COUNT(*) AS ?c) WHERE { ?s ?p ?o }" \ + "$TENTRIS_HOST/sparql") + +query_code=$(echo "$query_response" | tail -n1) +echo "Query response code: $query_code" + +if [[ "$query_code" =~ ^2[0-9][0-9]$ ]]; then + echo "✅ Query successful" + echo "Response: $(echo "$query_response" | head -n -1)" +else + echo "❌ Query failed (HTTP $query_code)" + echo "Response: $(echo "$query_response" | head -n -1)" +fi + +echo "" +echo "=== Test Complete ===" + +# Clean up cookie file +rm -f "$COOKIE_FILE" diff --git a/scripts/upload_all_to_tentris.sh b/scripts/upload_all_to_tentris.sh new file mode 100755 index 0000000..c35b8dd --- /dev/null +++ b/scripts/upload_all_to_tentris.sh @@ -0,0 +1,137 @@ +#!/bin/bash + +# --- CONFIGURATION --- +TENTRIS_HOST="http://128.178.219.51:7502" +USERNAME="admin" +PASSWORD="shrekislife" +COOKIE_FILE="/tmp/tentris-cookie" +DATA_DIR="/home/rmfranken/git-metadata-extractor/data/3_batch_18112025/3_batch/converted" +PYTHON_CMD="/home/rmfranken/git-metadata-extractor/.venv/bin/python" +TEMP_DIR="/tmp/tentris_batch" + +# --- END CONFIGURATION --- + +echo "=== Tentris Batch Upload ===" +echo "Data directory: $DATA_DIR" +echo "" + +# Create temp directory +mkdir -p "$TEMP_DIR" + +# Step 1: Login +echo "Step 1: Logging in to Tentris..." +login_response=$(curl -s -w "\n%{http_code}" -c "$COOKIE_FILE" \ + --data "username=$USERNAME&password=$PASSWORD" \ + "$TENTRIS_HOST/login") + +login_code=$(echo "$login_response" | tail -n1) + +if [[ "$login_code" =~ ^(2[0-9][0-9]|303)$ ]]; then + echo "✅ Login successful" +else + echo "❌ Login failed (HTTP $login_code)" + exit 1 +fi + +echo "" + +# Step 2: Count files +cd "$DATA_DIR" || { echo "❌ Directory not found: $DATA_DIR"; exit 1; } +total_files=$(ls -1 *.jsonld 2>/dev/null | wc -l) + +if [ "$total_files" -eq 0 ]; then + echo "❌ No JSON-LD files found in $DATA_DIR" + exit 1 +fi + +echo "Found $total_files JSON-LD files to upload" +echo "" + +# Step 3: Process each file +count=0 +success=0 +failed=0 +failed_files=() + +for jsonld_file in *.jsonld; do + if [ -f "$jsonld_file" ]; then + count=$((count + 1)) + echo "[$count/$total_files] Processing: $jsonld_file" + + # Convert to Turtle + turtle_file="$TEMP_DIR/$(basename "$jsonld_file" .jsonld).ttl" + + $PYTHON_CMD << PYEOF +import sys +from rdflib import Graph + +try: + g = Graph() + g.parse("$DATA_DIR/$jsonld_file", format="json-ld") + + with open("$turtle_file", "w", encoding="utf-8") as f: + f.write(g.serialize(format="turtle")) + + print(f" ✅ Converted to Turtle ({len(g)} triples)") + sys.exit(0) +except Exception as e: + print(f" ❌ Conversion failed: {e}") + sys.exit(1) +PYEOF + + if [ $? -ne 0 ]; then + echo " ❌ Skipping due to conversion error" + failed=$((failed + 1)) + failed_files+=("$jsonld_file (conversion failed)") + continue + fi + + # Upload to Tentris + upload_response=$(curl -s -w "\n%{http_code}" -b "$COOKIE_FILE" \ + -X POST \ + -H "Content-Type: text/turtle" \ + --data-binary "@$turtle_file" \ + "$TENTRIS_HOST/graph-store?default") + + upload_code=$(echo "$upload_response" | tail -n1) + + if [[ "$upload_code" =~ ^2[0-9][0-9]$ ]]; then + echo " ✅ Uploaded successfully (HTTP $upload_code)" + success=$((success + 1)) + # Clean up temp file + rm -f "$turtle_file" + else + echo " ❌ Upload failed (HTTP $upload_code)" + echo " Response: $(echo "$upload_response" | head -n -1 | head -c 100)" + failed=$((failed + 1)) + failed_files+=("$jsonld_file (HTTP $upload_code)") + fi + + # Small delay to avoid overwhelming the server + sleep 0.1 + echo "" + fi +done + +# Cleanup +rm -f "$COOKIE_FILE" +rmdir "$TEMP_DIR" 2>/dev/null + +# Summary +echo "=== Upload Complete ===" +echo "Total files: $total_files" +echo "Successful: $success" +echo "Failed: $failed" + +if [ $failed -gt 0 ]; then + echo "" + echo "Failed files:" + for file in "${failed_files[@]}"; do + echo " - $file" + done + exit 1 +else + echo "" + echo "✅ All files uploaded successfully!" + exit 0 +fi diff --git a/src/agents/academic_catalog_enrichment.py b/src/agents/academic_catalog_enrichment.py deleted file mode 100644 index fc237eb..0000000 --- a/src/agents/academic_catalog_enrichment.py +++ /dev/null @@ -1,519 +0,0 @@ -""" -Academic Catalog Enrichment Agent - -This agent searches academic catalogs (Infoscience, OpenAlex, EPFL Graph) to find -related publications, persons, and organizational units. -""" - -import logging -from typing import Any - -from pydantic_ai import Agent - -from ..context.infoscience import ( - get_author_publications_tool, - search_infoscience_authors_tool, - search_infoscience_labs_tool, - search_infoscience_publications_tool, -) -from ..data_models.academic_catalog import ( - AcademicCatalogEnrichmentResult, - AcademicCatalogRelation, - CatalogType, - EntityType, -) -from .url_validation import validate_infoscience_url -from ..llm.model_config import ( - create_pydantic_ai_model, - load_model_config, - validate_config, -) -from ..utils.token_counter import estimate_tokens_from_messages -from .academic_catalog_prompts import ( - academic_catalog_system_prompt, - get_organization_academic_catalog_prompt, - get_repository_academic_catalog_prompt, - get_user_academic_catalog_prompt, -) - -logger = logging.getLogger(__name__) - -# Load model configuration for academic catalog enrichment -academic_catalog_configs = load_model_config("run_academic_catalog_enrichment") - -# Validate configurations -for config in academic_catalog_configs: - if not validate_config(config): - logger.error( - f"Invalid configuration for academic catalog enrichment: {config}", - ) - raise ValueError("Invalid model configuration") - -# Track active agents for cleanup -_active_catalog_agents = [] - - -def create_academic_catalog_agent(config: dict) -> Agent: - """Create an academic catalog enrichment agent from configuration.""" - model = create_pydantic_ai_model(config) - - # Define tools for the agent - tools = [ - search_infoscience_publications_tool, - search_infoscience_authors_tool, - search_infoscience_labs_tool, - get_author_publications_tool, - ] - - agent = Agent( - model=model, - output_type=AcademicCatalogEnrichmentResult, - system_prompt=academic_catalog_system_prompt, - tools=tools, - ) - - # Track agent for cleanup - _active_catalog_agents.append(agent) - - return agent - - -async def cleanup_catalog_agents(): - """Cleanup academic catalog enrichment agents to free memory.""" - global _active_catalog_agents - for agent in _active_catalog_agents: - try: - # Close/cleanup if the agent has such methods - if hasattr(agent, "close"): - await agent.close() - except Exception as e: - logger.warning(f"Error cleaning up catalog agent: {e}") - _active_catalog_agents = [] - - -async def run_agent_with_fallback( - agent_configs: list[dict], - prompt: str, -) -> Any: - """ - Run the academic catalog enrichment agent with fallback across multiple models. - - Args: - agent_configs: List of model configurations to try - prompt: The enrichment prompt - - Returns: - Result dict with 'data' and 'usage' keys - """ - last_exception = None - - for idx, config in enumerate(agent_configs): - try: - logger.info( - f"Attempting academic catalog enrichment with model {idx + 1}/{len(agent_configs)}: {config.get('model')}", - ) - - # Create agent - agent = create_academic_catalog_agent(config) - - # Run the agent - result = await agent.run(prompt) - - # Extract output and usage - output = result.output - - # Extract token usage - usage_data = {} - if hasattr(result, "usage"): - usage = result.usage - input_tokens = getattr(usage, "input_tokens", 0) or 0 - output_tokens = getattr(usage, "output_tokens", 0) or 0 - - # Fallback to details field for certain models - if ( - input_tokens == 0 - and output_tokens == 0 - and hasattr(usage, "details") - ): - details = usage.details - if isinstance(details, dict): - input_tokens = details.get("input_tokens", 0) - output_tokens = details.get("output_tokens", 0) - - usage_data = { - "input_tokens": input_tokens, - "output_tokens": output_tokens, - } - - logger.info( - f"✓ Academic catalog enrichment succeeded with {input_tokens} input, {output_tokens} output tokens", - ) - - # Estimate tokens as fallback - response_text = ( - output.model_dump_json() if hasattr(output, "model_dump_json") else "" - ) - estimated = estimate_tokens_from_messages( - system_prompt=academic_catalog_system_prompt, - user_prompt=prompt, - response=response_text, - ) - - usage_data["estimated_input_tokens"] = estimated.get("input_tokens", 0) - usage_data["estimated_output_tokens"] = estimated.get("output_tokens", 0) - - return {"data": output, "usage": usage_data} - - except Exception as e: - logger.warning( - f"Academic catalog enrichment failed with model {config.get('model')}: {e}", - ) - last_exception = e - continue - - logger.error( - f"All academic catalog enrichment models failed. Last error: {last_exception}", - ) - raise (last_exception or Exception("All academic catalog enrichment models failed")) - - -async def _validate_infoscience_relations( - relations: list[AcademicCatalogRelation], -) -> list[AcademicCatalogRelation]: - """ - Validate and normalize Infoscience URLs in academic catalog relations. - - Args: - relations: List of AcademicCatalogRelation objects - - Returns: - Filtered list with validated relations (invalid ones removed) - """ - validated_relations = [] - - for relation in relations: - # Only validate Infoscience relations - if relation.catalogType != CatalogType.INFOSCIENCE: - validated_relations.append(relation) - continue - - # Get entity URL - entity_url = None - if hasattr(relation.entity, "url"): - entity_url = relation.entity.url - elif hasattr(relation.entity, "profile_url"): - entity_url = relation.entity.profile_url - elif isinstance(relation.entity, dict): - entity_url = relation.entity.get("url") or relation.entity.get("profile_url") - - if not entity_url: - logger.warning(f"Skipping relation without URL: {relation.get_display_name()}") - continue - - # Extract UUID from entity (handles both Pydantic models and dicts) - entity_uuid = None - if hasattr(relation.entity, "uuid"): - entity_uuid = relation.entity.uuid - elif isinstance(relation.entity, dict): - entity_uuid = relation.entity.get("uuid") - - # For Infoscience, UUID is mandatory - if not entity_uuid: - logger.warning( - f"Skipping Infoscience relation without UUID: {relation.get_display_name()}" - ) - continue - - try: - # Prepare expected entity data based on entity type - expected_entity = {} - if relation.entityType == EntityType.PUBLICATION: - if hasattr(relation.entity, "title"): - expected_entity["title"] = relation.entity.title - expected_entity["authors"] = getattr(relation.entity, "authors", []) - expected_entity["doi"] = getattr(relation.entity, "doi", None) - expected_entity["publication_date"] = getattr( - relation.entity, "publication_date", None - ) - expected_entity["lab"] = getattr(relation.entity, "lab", None) - elif isinstance(relation.entity, dict): - expected_entity = relation.entity - elif relation.entityType == EntityType.PERSON: - if hasattr(relation.entity, "name"): - expected_entity["name"] = relation.entity.name - expected_entity["affiliation"] = getattr( - relation.entity, "affiliation", None - ) - expected_entity["orcid"] = getattr(relation.entity, "orcid", None) - expected_entity["email"] = getattr(relation.entity, "email", None) - elif isinstance(relation.entity, dict): - expected_entity = relation.entity - elif relation.entityType == EntityType.ORGUNIT: - if hasattr(relation.entity, "name"): - expected_entity["name"] = relation.entity.name - expected_entity["parent_organization"] = getattr( - relation.entity, "parent_organization", None - ) - expected_entity["description"] = getattr( - relation.entity, "description", None - ) - elif isinstance(relation.entity, dict): - expected_entity = relation.entity - - # Validate Infoscience URL - validation_result = await validate_infoscience_url( - url=str(entity_url), - expected_entity=expected_entity, - entity_type=relation.entityType.value, - ctx=None, - ) - - if not validation_result.is_valid: - logger.warning( - f"⚠ Infoscience validation failed for {relation.get_display_name()}: " - f"{validation_result.justification}" - ) - # Skip invalid relation - continue - - # Update URL if normalized - if validation_result.normalized_url and validation_result.normalized_url != entity_url: - logger.info( - f"✓ Normalized Infoscience URL: {entity_url} -> {validation_result.normalized_url}" - ) - # Update entity URL - if hasattr(relation.entity, "url"): - relation.entity.url = validation_result.normalized_url - elif hasattr(relation.entity, "profile_url"): - relation.entity.profile_url = validation_result.normalized_url - elif isinstance(relation.entity, dict): - relation.entity["url"] = validation_result.normalized_url - - # Update confidence based on validation - if validation_result.confidence < 0.6: - logger.info( - f"⚠ Low confidence Infoscience match for {relation.get_display_name()}: " - f"confidence={validation_result.confidence:.2f}" - ) - # Reduce relation confidence - relation.confidence = min(relation.confidence, validation_result.confidence) - else: - logger.info( - f"✓ Infoscience validation passed for {relation.get_display_name()}: " - f"confidence={validation_result.confidence:.2f}" - ) - - validated_relations.append(relation) - - except Exception as e: - logger.error( - f"Error validating Infoscience URL for {relation.get_display_name()}: {e}", - exc_info=True, - ) - # Skip relation on error - continue - - return validated_relations - - -async def enrich_repository_academic_catalog( - repository_url: str, - repository_name: str, - description: str, - readme_excerpt: str, - authors: list = None, - organizations: list = None, -) -> dict: - """ - Enrich repository with academic catalog relations. - - Args: - repository_url: URL of the repository - repository_name: Name of the repository - description: Repository description - readme_excerpt: Excerpt from README - authors: List of identified author names - organizations: List of identified organization names - - Returns: - Dictionary with 'data' (AcademicCatalogEnrichmentResult) and 'usage' keys - """ - prompt = get_repository_academic_catalog_prompt( - repository_url=repository_url, - repository_name=repository_name, - description=description, - readme_excerpt=readme_excerpt, - authors=authors or [], - organizations=organizations or [], - ) - - logger.info( - f"🔍 Starting academic catalog enrichment for repository: {repository_name}", - ) - - try: - result = await run_agent_with_fallback(academic_catalog_configs, prompt) - - if result and result.get("data"): - enrichment_data = result["data"] - - # Validate Infoscience URLs in repository relations - logger.info("🔍 Validating Infoscience URLs in repository relations...") - enrichment_data.repository_relations = await _validate_infoscience_relations( - enrichment_data.repository_relations - ) - - logger.info( - f"✓ Found {len(enrichment_data.repository_relations)} validated repository relations" - ) - - # Validate Infoscience URLs in author relations - if hasattr(enrichment_data, "author_relations"): - logger.info("🔍 Validating Infoscience URLs in author relations...") - for author_name, relations in enrichment_data.author_relations.items(): - enrichment_data.author_relations[author_name] = await _validate_infoscience_relations( - relations - ) - logger.info( - f"✓ Validated author relations for {len(enrichment_data.author_relations)} authors" - ) - - # Validate Infoscience URLs in organization relations - if hasattr(enrichment_data, "organization_relations"): - logger.info("🔍 Validating Infoscience URLs in organization relations...") - for org_name, relations in enrichment_data.organization_relations.items(): - enrichment_data.organization_relations[org_name] = await _validate_infoscience_relations( - relations - ) - logger.info( - f"✓ Validated organization relations for {len(enrichment_data.organization_relations)} organizations" - ) - - return result - except Exception as e: - logger.error(f"Academic catalog enrichment failed: {e}") - # Return empty result instead of failing - return { - "data": AcademicCatalogEnrichmentResult( - repository_relations=[], - searchStrategy="Enrichment failed", - totalSearches=0, - ), - "usage": {"input_tokens": 0, "output_tokens": 0}, - } - - -async def enrich_user_academic_catalog( - username: str, - full_name: str, - bio: str, - organizations: list, -) -> dict: - """ - Enrich user with academic catalog relations. - - Args: - username: GitHub username - full_name: User's full name - bio: User's bio - organizations: List of organizations - - Returns: - Dictionary with 'data' (AcademicCatalogEnrichmentResult) and 'usage' keys - """ - prompt = get_user_academic_catalog_prompt( - username=username, - full_name=full_name, - bio=bio, - organizations=organizations, - ) - - logger.info(f"🔍 Starting academic catalog enrichment for user: {username}") - - try: - result = await run_agent_with_fallback(academic_catalog_configs, prompt) - - if result and result.get("data"): - enrichment_data = result["data"] - - # Validate Infoscience URLs in author relations - logger.info("🔍 Validating Infoscience URLs in author relations...") - for author_name, relations in enrichment_data.author_relations.items(): - enrichment_data.author_relations[author_name] = await _validate_infoscience_relations( - relations - ) - - logger.info( - f"✓ Found academic catalog relations for {len(enrichment_data.author_relations)} authors" - ) - - return result - except Exception as e: - logger.error(f"Academic catalog enrichment failed: {e}") - # Return empty result instead of failing - return { - "data": AcademicCatalogEnrichmentResult( - author_relations={}, - searchStrategy="Enrichment failed", - totalSearches=0, - ), - "usage": {"input_tokens": 0, "output_tokens": 0}, - } - - -async def enrich_organization_academic_catalog( - org_name: str, - description: str, - website: str, - members: list, -) -> dict: - """ - Enrich organization with academic catalog relations. - - Args: - org_name: Organization name - description: Organization description - website: Organization website - members: List of member usernames - - Returns: - Dictionary with 'data' (AcademicCatalogEnrichmentResult) and 'usage' keys - """ - prompt = get_organization_academic_catalog_prompt( - org_name=org_name, - description=description, - website=website, - members=members, - ) - - logger.info(f"🔍 Starting academic catalog enrichment for organization: {org_name}") - - try: - result = await run_agent_with_fallback(academic_catalog_configs, prompt) - - if result and result.get("data"): - enrichment_data = result["data"] - - # Validate Infoscience URLs in organization relations - logger.info("🔍 Validating Infoscience URLs in organization relations...") - for org_name, relations in enrichment_data.organization_relations.items(): - enrichment_data.organization_relations[org_name] = await _validate_infoscience_relations( - relations - ) - - logger.info( - f"✓ Found academic catalog relations for {len(enrichment_data.organization_relations)} organizations" - ) - - return result - except Exception as e: - logger.error(f"Academic catalog enrichment failed: {e}") - # Return empty result instead of failing - return { - "data": AcademicCatalogEnrichmentResult( - organization_relations={}, - searchStrategy="Enrichment failed", - totalSearches=0, - ), - "usage": {"input_tokens": 0, "output_tokens": 0}, - } diff --git a/src/agents/agents_management.py b/src/agents/agents_management.py index d356600..4046cad 100644 --- a/src/agents/agents_management.py +++ b/src/agents/agents_management.py @@ -72,12 +72,27 @@ def create_agent_from_config( """ model = create_pydantic_ai_model(config) + # Check if tools are allowed for this model configuration + # Default to True if not specified (backward compatibility) + allow_tools = config.get("allow_tools", True) + + # Only register tools if allowed and tools are provided + agent_tools = [] + if allow_tools and tools: + agent_tools = tools + elif not allow_tools and tools: + logger.warning( + f"Tools provided but allow_tools=False for {config.get('provider')}/{config.get('model')}. " + "Tools will not be registered.", + ) + # Create agent with the model and optional tools agent = Agent( model=model, output_type=output_type, # SoftwareSourceCode, system_prompt=system_prompt, # system_prompt_json, - tools=tools or [], # Register tools if provided + tools=agent_tools, # Register tools only if allowed + retries=3, # Allow model to retry up to 3 times on tool calls and output validation ) # Track agent for cleanup @@ -153,7 +168,126 @@ async def run_agent_with_retry( return result except Exception as e: last_exception = e - logger.warning(f"Agent run failed on attempt {attempt + 1}: {e}") + error_msg = str(e) + + # Log more details about validation errors + if "validation" in error_msg.lower() or "retries" in error_msg.lower(): + logger.error( + f"Agent run failed on attempt {attempt + 1} with validation error: {e}", + exc_info=True, # Include full traceback + ) + + # Try to extract Pydantic ValidationError details + validation_error = None + current_exc = e + + # Check the exception itself first + if hasattr(current_exc, "errors") and callable(current_exc.errors): + try: + validation_error = current_exc + logger.error("Found ValidationError in main exception") + except Exception: + pass + + # Traverse exception chain to find ValidationError + if not validation_error: + visited = set() + to_check = [e] + if hasattr(e, "__cause__") and e.__cause__: + to_check.append(e.__cause__) + if hasattr(e, "__context__") and e.__context__: + to_check.append(e.__context__) + + depth = 0 + while to_check and depth < 15: + current = to_check.pop(0) + if id(current) in visited: + continue + visited.add(id(current)) + + # Check if this is a ValidationError + if hasattr(current, "errors") and callable(current.errors): + try: + errors = current.errors() + if errors: + validation_error = current + logger.error( + f"Found ValidationError at depth {depth}", + ) + break + except Exception: + pass + + # Check for pydantic_core.ValidationError + if type( + current, + ).__name__ == "ValidationError" or "ValidationError" in str( + type(current), + ): + try: + if hasattr(current, "errors"): + errors = current.errors() + if errors: + validation_error = current + logger.error( + f"Found ValidationError (pydantic_core) at depth {depth}", + ) + break + except Exception: + pass + + # Add nested exceptions to check + if hasattr(current, "__cause__") and current.__cause__: + to_check.append(current.__cause__) + if hasattr(current, "__context__") and current.__context__: + to_check.append(current.__context__) + + depth += 1 + + # Log validation error details if found + if validation_error and hasattr(validation_error, "errors"): + try: + errors = validation_error.errors() + logger.error("=" * 80) + logger.error( + f"PYDANTIC VALIDATION ERRORS ({len(errors)} errors):", + ) + logger.error("=" * 80) + for i, error in enumerate(errors, 1): + field_path = " -> ".join( + str(loc) for loc in error.get("loc", []) + ) + logger.error(f"Error {i}:") + logger.error(f" Field path: {field_path}") + logger.error(f" Error type: {error.get('type', 'N/A')}") + logger.error(f" Message: {error.get('msg', 'N/A')}") + logger.error(f" Input value: {error.get('input', 'N/A')}") + if "ctx" in error: + logger.error(f" Context: {error['ctx']}") + logger.error("=" * 80) + except Exception as parse_err: + logger.error(f"Failed to parse validation errors: {parse_err}") + + # Try to extract raw LLM output from exception attributes + if hasattr(e, "args") and e.args: + for arg in e.args: + if isinstance(arg, dict): + logger.error(f"Exception args dict: {arg}") + elif isinstance(arg, str) and len(arg) > 100: + logger.error( + f"Exception args (first 500 chars): {arg[:500]}", + ) + + # Log exception attributes that might contain LLM output + for attr in ["output", "raw_output", "response", "data", "result"]: + if hasattr(e, attr): + value = getattr(e, attr) + if value is not None: + logger.error( + f"Exception.{attr}: {type(value)} = {str(value)[:500]}", + ) + else: + logger.warning(f"Agent run failed on attempt {attempt + 1}: {e}") if attempt < max_retries - 1: delay = get_retry_delay(attempt) diff --git a/src/agents/atomic_agents/__init__.py b/src/agents/atomic_agents/__init__.py new file mode 100644 index 0000000..48f2959 --- /dev/null +++ b/src/agents/atomic_agents/__init__.py @@ -0,0 +1,39 @@ +""" +Atomic agents for repository analysis pipeline. + +This module implements multiple two-stage agent pipelines: + +Main Analysis Pipeline: +1. Context compiler: Gathers repository information using tools +2. Structured output: Produces structured metadata from compiled context +3. Repository classifier: Classifies repository type and discipline +4. Organization identifier: Identifies related organizations and relationships + +Post-Enrichment Pipelines: +5. Linked entities searcher: Searches academic catalogs with tools → structures results +6. EPFL final checker: Compiles enriched data → assesses EPFL relationship +""" + +from .context_compiler import compile_repository_context +from .epfl_final_checker import ( + assess_final_epfl_relationship, + compile_enriched_data_for_epfl, +) +from .linked_entities_searcher import ( + search_academic_catalogs, + structure_linked_entities, +) +from .organization_identifier import identify_related_organizations +from .repository_classifier import classify_repository_type_and_discipline +from .structured_output import generate_structured_output + +__all__ = [ + "compile_repository_context", + "generate_structured_output", + "classify_repository_type_and_discipline", + "identify_related_organizations", + "compile_enriched_data_for_epfl", + "assess_final_epfl_relationship", + "search_academic_catalogs", + "structure_linked_entities", +] diff --git a/src/agents/atomic_agents/context_compiler.py b/src/agents/atomic_agents/context_compiler.py new file mode 100644 index 0000000..74de43d --- /dev/null +++ b/src/agents/atomic_agents/context_compiler.py @@ -0,0 +1,290 @@ +""" +Context compiler agent - First stage of atomic agent pipeline. + +This agent uses tools to gather comprehensive repository information +and compile it into a markdown document for the next agent. +""" + +import logging +from typing import Any, Dict, Optional + +# Tools removed - context compiler only uses repository content and GIMIE data +# from ...context.infoscience import ( +# get_author_publications_tool, +# search_infoscience_publications_tool, +# ) +from ...llm.model_config import load_model_config, validate_config +from ...utils.token_counter import ( + estimate_tokens_with_tools, +) +from ..agents_management import run_agent_with_fallback +from .models import CompiledContext + +logger = logging.getLogger(__name__) + +# Load model configurations for context compilation +CONTEXT_COMPILER_CONFIGS = load_model_config("run_context_compiler") + +# Validate configurations +for config in CONTEXT_COMPILER_CONFIGS: + if not validate_config(config): + logger.error(f"Invalid configuration for context compiler: {config}") + raise ValueError("Invalid model configuration") + +# System prompt for context compiler +CONTEXT_COMPILER_SYSTEM_PROMPT = """ +You are an expert at gathering and compiling comprehensive information about software repositories. + +Your task is to: +1. Analyze the repository content provided (code, README, documentation, etc.) +2. Analyze the GIMIE metadata provided (if available) +3. Compile all information into a well-structured markdown document + +**Input Sources:** +- Repository content: Code, README, documentation files, and other repository files +- GIMIE metadata: Structured metadata extracted from the Git provider (GitHub/GitLab) + +**Output Format:** +Return ONLY a comprehensive markdown document (plain text, not JSON) that includes: +- Repository overview and description +- Key features and functionality +- Authors and contributors (with affiliations if mentioned in repository content or GIMIE data) +- Technologies and dependencies +- License information +- Any other relevant information from the repository content and GIMIE metadata + +The compiled context should be thorough and well-organized for the next agent to extract structured metadata. +Do NOT search for additional information outside of what is provided in the repository content and GIMIE metadata. + +**IMPORTANT:** Return ONLY the markdown document as plain text. Do not wrap it in JSON, do not add any explanatory text, do not use code blocks. Just return the raw markdown text. +""" + + +def get_context_compiler_prompt( + repo_url: str, + repository_content: str, + gimie_data: Optional[str] = None, +) -> str: + """ + Generate prompt for context compiler agent. + + Args: + repo_url: Repository URL + repository_content: Extracted repository content (markdown) + gimie_data: Optional GIMIE metadata + + Returns: + Formatted prompt string + """ + prompt = f"""Compile comprehensive information about this repository: + +**Repository URL:** {repo_url} + +**Repository Content:** +{repository_content} +""" + + if gimie_data: + # Parse GIMIE data to extract structured authors/orgs if available + try: + import json as json_module + + gimie_dict = json_module.loads(gimie_data) + + # Extract structured authors and organizations if available + extracted_authors = gimie_dict.get("extracted_authors", []) + extracted_orgs = gimie_dict.get("extracted_organizations", []) + + prompt += f""" + +**GIMIE Metadata (extracted from Git provider):** +{gimie_data} +""" + + # Add structured authors/orgs section if available + if extracted_authors or extracted_orgs: + prompt += f""" + +**Pre-extracted Authors and Organizations from GIMIE:** + +**Authors ({len(extracted_authors)}):** +{json_module.dumps(extracted_authors, indent=2)} + +**Organizations ({len(extracted_orgs)}):** +{json_module.dumps(extracted_orgs, indent=2)} + +**Important:** These authors and organizations have been pre-extracted from GIMIE with their affiliations already resolved. Use this structured data when identifying authors and organizations in your compiled context. The affiliations field in authors may contain organization objects (with id, legalName, etc.) or organization name strings. +""" + except Exception as e: + # If parsing fails, just include raw GIMIE data + logger.warning(f"Failed to parse GIMIE data for structured extraction: {e}") + prompt += f""" + +**GIMIE Metadata (extracted from Git provider):** +{gimie_data} +""" + logger.debug("GIMIE data included in context compiler prompt") + else: + logger.debug("No GIMIE data to include in context compiler prompt") + + prompt += """ + +Please: +1. Analyze the repository content provided +2. Analyze the GIMIE metadata provided (if available) +3. Compile all information into a comprehensive markdown document + +Focus on extracting and organizing information from the provided sources to help extract structured metadata in the next step. +Do NOT search for additional information - only use what is provided in the repository content and GIMIE metadata. + +**IMPORTANT:** Return ONLY the markdown document as plain text. Do not wrap it in JSON, do not add any explanatory text, do not use code blocks. Just return the raw markdown text. +""" + + logger.debug(f"Context compiler prompt length: {len(prompt)} chars") + return prompt + + +async def compile_repository_context( + repo_url: str, + repository_content: str, + gimie_data: Optional[str] = None, + git_authors: Optional[list] = None, +) -> Dict[str, Any]: + """ + Compile repository context using tools to gather comprehensive information. + + Args: + repo_url: Repository URL + repository_content: Extracted repository content + gimie_data: Optional GIMIE metadata + git_authors: Optional list of git authors + + Returns: + Dictionary with 'data' (CompiledContext) and 'usage' (dict with token info) + """ + # Create context for the agent + agent_context = { + "repo_url": repo_url, + "repository_content": repository_content, + "gimie_data": gimie_data, + "git_authors": git_authors or [], + } + + # Prepare the prompt + prompt = get_context_compiler_prompt(repo_url, repository_content, gimie_data) + + # No tools for context compilation - only use repository content and GIMIE data + tools = [] + + try: + # Run agent with fallback across multiple models + # Use str as output type - agent returns markdown text + result = await run_agent_with_fallback( + CONTEXT_COMPILER_CONFIGS, + prompt, + agent_context, + str, # Simple string output - just markdown text + CONTEXT_COMPILER_SYSTEM_PROMPT, + tools, + ) + + # Extract the markdown string from PydanticAI result + if hasattr(result, "output"): + markdown_content = result.output + else: + markdown_content = result + + # Convert string to CompiledContext + if isinstance(markdown_content, str): + compiled_context = CompiledContext( + markdown_content=markdown_content, + repository_url=repo_url, + summary=None, + ) + else: + # Fallback if we get something unexpected + compiled_context = CompiledContext( + markdown_content=str(markdown_content), + repository_url=repo_url, + summary=None, + ) + + # Estimate tokens from prompt and response + response_text = "" + if hasattr(compiled_context, "model_dump_json"): + response_text = compiled_context.model_dump_json() + elif isinstance(compiled_context, dict): + import json as json_module + + response_text = json_module.dumps(compiled_context) + elif isinstance(compiled_context, str): + response_text = compiled_context + + # Extract usage information from the result + input_tokens = 0 + output_tokens = 0 + tool_calls_count = 0 + + if hasattr(result, "usage"): + usage = result.usage + input_tokens = getattr(usage, "input_tokens", 0) or 0 + output_tokens = getattr(usage, "output_tokens", 0) or 0 + tool_calls_count = getattr(usage, "tool_calls", 0) or 0 + + # Fallback to details field for certain models + if input_tokens == 0 and output_tokens == 0 and hasattr(usage, "details"): + details = usage.details + if isinstance(details, dict): + input_tokens = details.get("input_tokens", 0) or 0 + output_tokens = details.get("output_tokens", 0) or 0 + + # Calculate estimates with tool call support (always, for validation/fallback) + estimated = estimate_tokens_with_tools( + system_prompt=CONTEXT_COMPILER_SYSTEM_PROMPT, + user_prompt=prompt, + response=response_text, + tool_calls=tool_calls_count, + tool_results_text=None, + ) + + # Use estimates as primary when API returns 0 + if input_tokens == 0 and output_tokens == 0: + logger.warning( + "API returned 0 tokens, using tiktoken estimates as primary counts", + ) + input_tokens = estimated.get("input_tokens", 0) + output_tokens = estimated.get("output_tokens", 0) + + usage_data = { + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "estimated_input_tokens": estimated.get("input_tokens", 0), + "estimated_output_tokens": estimated.get("output_tokens", 0), + } + + # Log compiled context size + if hasattr(compiled_context, "markdown_content"): + content_size = len(compiled_context.markdown_content) + elif ( + isinstance(compiled_context, dict) + and "markdown_content" in compiled_context + ): + content_size = len(compiled_context.get("markdown_content", "")) + else: + content_size = 0 + + logger.info( + f"Context compilation completed: {content_size:,} chars of markdown", + ) + + return { + "data": compiled_context, + "usage": usage_data, + } + + except Exception as e: + logger.error(f"Context compilation failed: {e}", exc_info=True) + return { + "data": None, + "usage": None, + } diff --git a/src/agents/atomic_agents/epfl_final_checker.py b/src/agents/atomic_agents/epfl_final_checker.py new file mode 100644 index 0000000..ad96507 --- /dev/null +++ b/src/agents/atomic_agents/epfl_final_checker.py @@ -0,0 +1,383 @@ +""" +EPFL final checker agent - Post-enrichment EPFL assessment using atomic agent pipeline. + +This module implements a two-stage assessment for EPFL relationship after all enrichments: +1. Context compiler: Compiles enriched data into markdown document +2. Structured assessment: Analyzes markdown and produces EPFL assessment +""" + +import json +import logging +from typing import Any, Dict + +from ...llm.model_config import load_model_config, validate_config +from ...utils.token_counter import ( + estimate_tokens_from_messages, + estimate_tokens_with_tools, +) +from ..agents_management import run_agent_with_fallback +from .models import EnrichedDataContext, EPFLAssessment + +logger = logging.getLogger(__name__) + +# Load model configurations for EPFL final assessment +EPFL_FINAL_CHECKER_CONFIGS = load_model_config("run_epfl_final_checker") + +# Validate configurations +for config in EPFL_FINAL_CHECKER_CONFIGS: + if not validate_config(config): + logger.error(f"Invalid configuration for EPFL final checker: {config}") + raise ValueError("Invalid model configuration") + +# System prompt for context compilation stage +EPFL_CONTEXT_COMPILER_SYSTEM_PROMPT = """ +You are an expert at compiling comprehensive information about software repositories and their relationships to EPFL (École Polytechnique Fédérale de Lausanne). + +Your task is to: +1. Analyze the enriched repository data provided (with ORCID affiliations, ROR organizations, linked entities) +2. Extract and organize all information relevant to EPFL relationship assessment +3. Compile everything into a well-structured markdown document + +**Focus on EPFL-relevant information:** +- Authors with @epfl.ch email addresses +- ORCID affiliations mentioning EPFL, SDSC, or EPFL labs +- Organizations with EPFL in their name or ROR data +- Linked entities (publications, author profiles) from Infoscience +- README mentions of EPFL, SDSC, or EPFL-related projects +- Related organizations that include EPFL + +**Output Format:** +Return ONLY a comprehensive markdown document (plain text, not JSON) organized with clear sections: +- Repository Overview +- Authors and Affiliations (with EPFL connections highlighted) +- Organizations (with EPFL relationships) +- Linked Entities (Infoscience publications/profiles if available) +- Other EPFL Evidence (README mentions, etc.) + +**IMPORTANT:** Return ONLY the markdown document as plain text. Do not wrap it in JSON, do not add explanatory text, do not use code blocks. Just return the raw markdown text. +""" + +# System prompt for EPFL assessment stage +EPFL_ASSESSMENT_SYSTEM_PROMPT = """ +You are an expert at assessing relationships between software repositories and EPFL (École Polytechnique Fédérale de Lausanne). + +Your task is to: +1. Analyze the compiled enriched data provided +2. Systematically identify ALL evidence of EPFL relationship +3. Calculate cumulative confidence score based on evidence weights +4. Provide detailed justification listing all evidence found + +**Evidence Types and Weights:** +- EMAIL_DOMAIN (@epfl.ch addresses): 0.4 +- ORCID_EMPLOYMENT (ORCID employment at EPFL): 0.3 +- INFOSCIENCE_ENTITY (found in Infoscience database): 0.4 +- BIO_MENTION (mentions EPFL/SDSC in bio/description): 0.25 +- README_MENTION (mentions EPFL/SDSC in README): 0.25 +- COMPANY_FIELD (company field mentions EPFL): 0.25 +- ORGANIZATION_MEMBERSHIP (member of EPFL GitHub orgs): 0.25 +- RELATED_ORGANIZATION (related org is EPFL from ROR): 0.25 +- LOCATION (location is Lausanne): 0.15 + +**Confidence Calculation:** +- Sum all evidence weights +- Cap at 1.0 +- relatedToEPFL = true if confidence >= 0.5, false otherwise + +**Output:** +- relatedToEPFL: Boolean (true if confidence >= 0.5) +- relatedToEPFLConfidence: Float (0.0 to 1.0) +- relatedToEPFLJustification: String (comprehensive list of all evidence with contributions) +""" + + +def get_epfl_context_compiler_prompt(enriched_data: Dict[str, Any]) -> str: + """ + Generate prompt for EPFL context compilation. + + Args: + enriched_data: Complete repository data with all enrichments + + Returns: + Formatted prompt string + """ + prompt = f"""Compile comprehensive information about this repository's relationship to EPFL. + +**Enriched Repository Data:** +{json.dumps(enriched_data, indent=2, default=str)} + +Please: +1. Analyze all the enriched data provided +2. Extract and organize information relevant to EPFL relationship assessment +3. Focus on authors, affiliations, organizations, linked entities, and any EPFL mentions +4. Compile everything into a well-structured markdown document + +Highlight EPFL connections clearly in each section. + +**IMPORTANT:** Return ONLY the markdown document as plain text. Do not wrap it in JSON, do not add explanatory text, do not use code blocks. Just return the raw markdown text. +""" + + return prompt + + +def get_epfl_assessment_prompt(enriched_context: EnrichedDataContext) -> str: + """ + Generate prompt for EPFL assessment. + + Args: + enriched_context: Compiled enriched data context + + Returns: + Formatted prompt string + """ + prompt = f"""Assess the EPFL relationship for this repository using ALL enriched data. + +**Compiled Enriched Data:** +{enriched_context.markdown_content} + +**Repository URL:** {enriched_context.repository_url} + +Please: +1. Systematically examine ALL the enriched data +2. Identify EVERY piece of evidence related to EPFL +3. Calculate cumulative confidence score (sum of evidence weights, max 1.0) +4. Determine boolean based on confidence threshold (>= 0.5 = true, < 0.5 = false) +5. Write comprehensive justification listing all evidence with confidence contributions + +Be thorough and explicit about all evidence found and how each contributes to the confidence score. +""" + + return prompt + + +async def compile_enriched_data_for_epfl( + enriched_data: Dict[str, Any], + repository_url: str, +) -> Dict[str, Any]: + """ + Compile enriched repository data into markdown for EPFL assessment. + + Stage 1 of the EPFL final assessment pipeline. + + Args: + enriched_data: Complete repository data with all enrichments + repository_url: Repository URL + + Returns: + Dictionary with 'data' (EnrichedDataContext) and 'usage' (dict with token info) + """ + # Create context for the agent + agent_context = { + "enriched_data": enriched_data, + "repository_url": repository_url, + } + + # Prepare the prompt + prompt = get_epfl_context_compiler_prompt(enriched_data) + + # No tools for context compilation - only analyze existing enriched data + tools = [] + + try: + # Run agent with fallback across multiple models + # Use str as output type - agent returns markdown text + result = await run_agent_with_fallback( + EPFL_FINAL_CHECKER_CONFIGS, + prompt, + agent_context, + str, # Simple string output - just markdown text + EPFL_CONTEXT_COMPILER_SYSTEM_PROMPT, + tools, + ) + + # Extract the markdown string from PydanticAI result + if hasattr(result, "output"): + markdown_content = result.output + else: + markdown_content = result + + # Convert string to EnrichedDataContext + if isinstance(markdown_content, str): + enriched_context = EnrichedDataContext( + markdown_content=markdown_content, + repository_url=repository_url, + summary=None, + ) + else: + # Fallback if we get something unexpected + enriched_context = EnrichedDataContext( + markdown_content=str(markdown_content), + repository_url=repository_url, + summary=None, + ) + + # Estimate tokens from prompt and response + response_text = ( + markdown_content + if isinstance(markdown_content, str) + else str(markdown_content) + ) + + estimated = estimate_tokens_from_messages( + system_prompt=EPFL_CONTEXT_COMPILER_SYSTEM_PROMPT, + user_prompt=prompt, + response=response_text, + ) + + # Extract usage information from the result + usage_data = None + + if hasattr(result, "usage"): + usage = result.usage + input_tokens = getattr(usage, "input_tokens", 0) or 0 + output_tokens = getattr(usage, "output_tokens", 0) or 0 + + # Fallback to details field for certain models + if input_tokens == 0 and output_tokens == 0 and hasattr(usage, "details"): + details = usage.details + if isinstance(details, dict): + input_tokens = details.get("input_tokens", 0) + output_tokens = details.get("output_tokens", 0) + + usage_data = { + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "estimated_input_tokens": estimated.get("input_tokens", 0), + "estimated_output_tokens": estimated.get("output_tokens", 0), + } + + logger.info("EPFL enriched data context compilation completed successfully") + + # Debug: Log the compiled markdown content + if hasattr(enriched_context, "markdown_content"): + logger.debug("=" * 80) + logger.debug("EPFL ENRICHED CONTEXT MARKDOWN (Stage 1 Output):") + logger.debug("=" * 80) + logger.debug( + enriched_context.markdown_content[:1000] + "..." + if len(enriched_context.markdown_content) > 1000 + else enriched_context.markdown_content, + ) + logger.debug("=" * 80) + + return { + "data": enriched_context, + "usage": usage_data, + } + + except Exception as e: + logger.error( + f"EPFL enriched data context compilation failed: {e}", + exc_info=True, + ) + return { + "data": None, + "usage": None, + } + + +async def assess_final_epfl_relationship( + enriched_context: EnrichedDataContext, +) -> Dict[str, Any]: + """ + Assess EPFL relationship from compiled enriched data. + + Stage 2 of the EPFL final assessment pipeline. + + Args: + enriched_context: Compiled enriched data context from stage 1 + + Returns: + Dictionary with 'data' (EPFLAssessment) and 'usage' (dict with token info) + """ + # Create context for the agent + agent_context = { + "enriched_context": enriched_context, + } + + # Prepare the prompt + prompt = get_epfl_assessment_prompt(enriched_context) + + # No tools for EPFL assessment agent + tools = [] + + try: + # Run agent with fallback across multiple models + result = await run_agent_with_fallback( + EPFL_FINAL_CHECKER_CONFIGS, + prompt, + agent_context, + EPFLAssessment, + EPFL_ASSESSMENT_SYSTEM_PROMPT, + tools, # No tools for this agent + ) + + # Extract the output from PydanticAI result + if hasattr(result, "output"): + epfl_assessment = result.output + else: + epfl_assessment = result + + # Estimate tokens from prompt and response + response_text = "" + if hasattr(epfl_assessment, "model_dump_json"): + response_text = epfl_assessment.model_dump_json() + elif isinstance(epfl_assessment, dict): + response_text = json.dumps(epfl_assessment) + elif isinstance(epfl_assessment, str): + response_text = epfl_assessment + + # Extract usage information from the result + input_tokens = 0 + output_tokens = 0 + tool_calls_count = 0 + + if hasattr(result, "usage"): + usage = result.usage + input_tokens = getattr(usage, "input_tokens", 0) or 0 + output_tokens = getattr(usage, "output_tokens", 0) or 0 + tool_calls_count = getattr(usage, "tool_calls", 0) or 0 + + # Fallback to details field for certain models + if input_tokens == 0 and output_tokens == 0 and hasattr(usage, "details"): + details = usage.details + if isinstance(details, dict): + input_tokens = details.get("input_tokens", 0) or 0 + output_tokens = details.get("output_tokens", 0) or 0 + + # Calculate estimates with tool call support (always, for validation/fallback) + estimated = estimate_tokens_with_tools( + system_prompt=EPFL_ASSESSMENT_SYSTEM_PROMPT, + user_prompt=prompt, + response=response_text, + tool_calls=tool_calls_count, + tool_results_text=None, + ) + + # Use estimates as primary when API returns 0 + if input_tokens == 0 and output_tokens == 0: + logger.warning( + "API returned 0 tokens, using tiktoken estimates as primary counts", + ) + input_tokens = estimated.get("input_tokens", 0) + output_tokens = estimated.get("output_tokens", 0) + + usage_data = { + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "estimated_input_tokens": estimated.get("input_tokens", 0), + "estimated_output_tokens": estimated.get("output_tokens", 0), + } + + logger.info("Final EPFL relationship assessment completed successfully") + + return { + "data": epfl_assessment, + "usage": usage_data, + } + + except Exception as e: + logger.error(f"Final EPFL relationship assessment failed: {e}", exc_info=True) + return { + "data": None, + "usage": None, + } diff --git a/src/agents/atomic_agents/linked_entities_searcher.py b/src/agents/atomic_agents/linked_entities_searcher.py new file mode 100644 index 0000000..8236dff --- /dev/null +++ b/src/agents/atomic_agents/linked_entities_searcher.py @@ -0,0 +1,466 @@ +""" +Linked entities searcher agent - Academic catalog search using atomic agent pipeline. + +This module implements a two-stage pipeline for searching academic catalogs: +1. Context compiler with tools: Searches Infoscience for repository and authors +2. Structured output: Organizes search results into structured format +""" + +import json +import logging +from typing import Any, Dict, List + +from ...context.infoscience import ( + search_infoscience_publications_tool, +) +from ...data_models.conversion import create_simplified_model +from ...data_models.linked_entities import linkedEntitiesEnrichmentResult +from ...llm.model_config import load_model_config, validate_config +from ...utils.token_counter import ( + estimate_tokens_with_tools, +) +from ..agents_management import run_agent_with_fallback +from .models import LinkedEntitiesContext + +logger = logging.getLogger(__name__) + +# Load model configurations for linked entities search +LINKED_ENTITIES_SEARCHER_CONFIGS = load_model_config("run_linked_entities_searcher") + +# Generate simplified model dynamically from linkedEntitiesEnrichmentResult +# Only include fields needed for LLM output (repository_relations only) +# Cache it at module level to avoid regenerating on every call +LINKED_ENTITIES_FIELDS = [ + "repository_relations", + # Note: author_relations and organization_relations are handled in optional enrichment +] + +( + _SIMPLIFIED_LINKED_ENTITIES_MODEL, + _LINKED_ENTITIES_UNION_METADATA, +) = create_simplified_model( + linkedEntitiesEnrichmentResult, + field_filter=LINKED_ENTITIES_FIELDS, +) + +# Validate configurations +for config in LINKED_ENTITIES_SEARCHER_CONFIGS: + if not validate_config(config): + logger.error(f"Invalid configuration for linked entities searcher: {config}") + raise ValueError("Invalid model configuration") + +# System prompt for search stage (with tools) +LINKED_ENTITIES_SEARCH_SYSTEM_PROMPT = """ +You are an expert at searching academic catalogs to find publications related to software repositories. + +Your task is to: +1. Search Infoscience (EPFL's research repository) for the repository/tool name +2. Find relevant publications and related entities +3. Compile search results into a comprehensive markdown document + +**Available Tools:** +- search_infoscience_publications_tool(query, max_results): Search for publications by repository/tool name + +**Search Strategy:** +1. Search for the repository/tool name to find publications about or using it (max 5 results) +2. Be strategic - ONE search per repository, avoid repetition +3. If a search returns 0 results, STOP searching (it's not in Infoscience) + +**IMPORTANT CONSTRAINTS:** +- Maximum 5 results per search +- ONE search for the repository (don't try variations or search again) +- Cache automatically stores results (including empty results) +- Accept when information is not found rather than keep searching + +**Output Format:** +Return ONLY a comprehensive markdown document (plain text, not JSON) with: +- Repository/Tool Search Results section +- Clear indication when no results are found + +**IMPORTANT:** Return ONLY the markdown document as plain text. Do not wrap it in JSON, do not add explanatory text, do not use code blocks. Just return the raw markdown text. +""" + +# System prompt for structuring stage (no tools) +LINKED_ENTITIES_STRUCTURE_SYSTEM_PROMPT = """ +You are an expert at organizing academic catalog search results into structured data. + +Your task is to: +1. Analyze the search results markdown provided +2. Extract and organize results for repository-level entities +3. Structure the data according to the provided schema + +**Important:** +- Use ONLY primitive types: strings, numbers, lists, and dictionaries +- URLs must be strings (not HttpUrl objects) +- Do not include fields not in the schema +- Organize results into repository_relations (publications about the repository/tool) +- **catalogType** MUST be one of: "infoscience", "openalex", or "epfl_graph" (for Infoscience results, use "infoscience") +- **entityType** MUST be one of: "publication", "person", or "orgunit" + +**CRITICAL: Entity Field Handling:** +The entity field is a Union that gets split into THREE separate fields based on entity type: +- **entityInfosciencePublication**: Populate ONLY when entityType is "publication" - leave the other two fields EMPTY/OMITTED +- **entityInfoscienceAuthor**: Populate ONLY when entityType is "person" - leave the other two fields EMPTY/OMITTED +- **entityInfoscienceLab**: Populate ONLY when entityType is "orgunit" - leave the other two fields EMPTY/OMITTED + +**CRITICAL RULE: Only populate ONE of these three fields per relation - the one matching the entityType!** +- If entityType="publication", ONLY populate entityInfosciencePublication (do NOT populate entityInfoscienceAuthor or entityInfoscienceLab) +- If entityType="person", ONLY populate entityInfoscienceAuthor (do NOT populate entityInfosciencePublication or entityInfoscienceLab) +- If entityType="orgunit", ONLY populate entityInfoscienceLab (do NOT populate entityInfosciencePublication or entityInfoscienceAuthor) + +**List Fields:** +- For list fields like "subjects", "authors", "keywords": Use empty array [] if no data, NEVER use null/None + +For each entity type, include these fields: + +**entityInfosciencePublication (when entityType="publication"):** + - title: Publication title + - authors: List of author names + - url: Full Infoscience URL + - uuid: Entity UUID + - publication_date: Publication date (if available) + +**entityInfoscienceAuthor (when entityType="person"):** + - name: Person's full name + - profile_url: Full Infoscience profile URL + - uuid: Entity UUID + - email: Email address (if available) + - orcid: ORCID identifier (if available) + - affiliation: Primary affiliation/lab (if available) + +**entityInfoscienceOrgUnit (when entityType="orgunit"):** + - name: Lab/organization name + - url: Full Infoscience URL + - uuid: Entity UUID + +**Example for a publication from Infoscience:** +```json +{ + "catalogType": "infoscience", + "entityType": "publication", + "entityInfosciencePublication": { + "type": "InfosciencePublication", + "title": "DeepLabCut: markerless pose estimation", + "authors": ["Alexander Mathis", "Mackenzie Mathis"], + "url": "https://infoscience.epfl.ch/entities/publication/12345", + "uuid": "12345-67890", + "publication_date": "2020-01-15", + "subjects": ["Computer Science", "Machine Learning"] + }, + "confidence": 0.9, + "justification": "Found publication about the repository in Infoscience" +} +``` +Note: Only entityInfosciencePublication is populated. Do NOT include entityInfoscienceAuthor or entityInfoscienceOrgUnit fields at all. + +**Output Format:** +Return a JSON object matching the provided schema exactly. +""" + + +def get_linked_entities_search_prompt( + repository_name: str, + author_names: List[str], # Kept for backward compatibility but unused +) -> str: + """ + Generate prompt for linked entities search. + + Args: + repository_name: Repository or tool name to search for + author_names: Unused (kept for backward compatibility) + + Returns: + Formatted prompt string + """ + prompt = f"""Search academic catalogs for this repository. + +**Repository/Tool Name:** {repository_name} + +Please: +1. Search for the repository/tool name in publications (max 5 results) +2. Compile all search results into a well-organized markdown document + +Use the provided tools strategically - ONE search for the repository, max 5 results. + +**IMPORTANT:** Return ONLY the markdown document as plain text. Do not wrap it in JSON, do not add explanatory text, do not use code blocks. Just return the raw markdown text. +""" + + return prompt + + +def get_linked_entities_structure_prompt( + search_context: LinkedEntitiesContext, + schema: Dict[str, Any], +) -> str: + """ + Generate prompt for structuring linked entities results. + + Args: + search_context: Compiled search results context + schema: Simplified schema definition + + Returns: + Formatted prompt string + """ + prompt = f"""Organize the academic catalog search results into structured data. + +**Search Results Markdown:** +{search_context.markdown_content} + +**Repository Name:** {search_context.repository_name} + +**Expected Output Schema:** +{json.dumps(schema, indent=2)} + +Please extract and organize the search results according to the schema. +Organize by: +- repository_relations: Publications/entities about the repository itself + +Use only primitive types (strings, numbers, lists, dicts). +""" + + return prompt + + +async def search_academic_catalogs( + repository_name: str, +) -> Dict[str, Any]: + """ + Search academic catalogs using Infoscience tools. + + Stage 1 of the linked entities enrichment pipeline. + + Args: + repository_name: Repository or tool name to search for + + Returns: + Dictionary with 'data' (LinkedEntitiesContext) and 'usage' (dict with token info) + """ + # Create context for the agent + agent_context = { + "repository_name": repository_name, + } + + # Prepare the prompt (no author names) + prompt = get_linked_entities_search_prompt(repository_name, []) + + # Add Infoscience search tools (only publications) + tools = [ + search_infoscience_publications_tool, + ] + + try: + # Run agent with fallback across multiple models + # Use str as output type - agent returns markdown text + result = await run_agent_with_fallback( + LINKED_ENTITIES_SEARCHER_CONFIGS, + prompt, + agent_context, + str, # Simple string output - just markdown text + LINKED_ENTITIES_SEARCH_SYSTEM_PROMPT, + tools, + ) + + # Extract the markdown string from PydanticAI result + if hasattr(result, "output"): + markdown_content = result.output + else: + markdown_content = result + + # Convert string to LinkedEntitiesContext + if isinstance(markdown_content, str): + search_context = LinkedEntitiesContext( + markdown_content=markdown_content, + repository_name=repository_name, + author_names=[], # No author search in atomic pipeline + ) + else: + # Fallback if we get something unexpected + search_context = LinkedEntitiesContext( + markdown_content=str(markdown_content), + repository_name=repository_name, + author_names=[], # No author search in atomic pipeline + ) + + # Estimate tokens from prompt and response + response_text = ( + markdown_content + if isinstance(markdown_content, str) + else str(markdown_content) + ) + + # Extract usage information from the result + input_tokens = 0 + output_tokens = 0 + tool_calls_count = 0 + + if hasattr(result, "usage"): + usage = result.usage + input_tokens = getattr(usage, "input_tokens", 0) or 0 + output_tokens = getattr(usage, "output_tokens", 0) or 0 + tool_calls_count = getattr(usage, "tool_calls", 0) or 0 + + # Fallback to details field for certain models + if input_tokens == 0 and output_tokens == 0 and hasattr(usage, "details"): + details = usage.details + if isinstance(details, dict): + input_tokens = details.get("input_tokens", 0) or 0 + output_tokens = details.get("output_tokens", 0) or 0 + + # Calculate estimates with tool call support (always, for validation/fallback) + estimated = estimate_tokens_with_tools( + system_prompt=LINKED_ENTITIES_SEARCH_SYSTEM_PROMPT, + user_prompt=prompt, + response=response_text, + tool_calls=tool_calls_count, + tool_results_text=None, + ) + + # Use estimates as primary when API returns 0 + if input_tokens == 0 and output_tokens == 0: + logger.warning( + "API returned 0 tokens, using tiktoken estimates as primary counts", + ) + input_tokens = estimated.get("input_tokens", 0) + output_tokens = estimated.get("output_tokens", 0) + + usage_data = { + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "estimated_input_tokens": estimated.get("input_tokens", 0), + "estimated_output_tokens": estimated.get("output_tokens", 0), + } + + # Log search results size + if hasattr(search_context, "markdown_content"): + search_markdown_size = len(search_context.markdown_content) + else: + search_markdown_size = 0 + + logger.info( + f"Academic catalog search completed: {search_markdown_size:,} chars of results", + ) + + return { + "data": search_context, + "usage": usage_data, + } + + except Exception as e: + logger.error(f"Academic catalog search failed: {e}", exc_info=True) + return { + "data": None, + "usage": None, + } + + +async def structure_linked_entities( + search_context: LinkedEntitiesContext, + schema: Dict[str, Any], +) -> Dict[str, Any]: + """ + Structure linked entities search results. + + Stage 2 of the linked entities enrichment pipeline. + + Args: + search_context: Compiled search results from stage 1 + schema: Simplified schema definition + + Returns: + Dictionary with 'data' (SimplifiedLinkedEntitiesResult) and 'usage' (dict with token info) + """ + # Create context for the agent + agent_context = { + "search_context": search_context, + "schema": schema, + } + + # Prepare the prompt + prompt = get_linked_entities_structure_prompt(search_context, schema) + + # No tools for structured output agent + tools = [] + + try: + # Run agent with fallback across multiple models + # Use dynamically generated simplified model + result = await run_agent_with_fallback( + LINKED_ENTITIES_SEARCHER_CONFIGS, + prompt, + agent_context, + _SIMPLIFIED_LINKED_ENTITIES_MODEL, + LINKED_ENTITIES_STRUCTURE_SYSTEM_PROMPT, + tools, # No tools for this agent + ) + + # Extract the output from PydanticAI result + if hasattr(result, "output"): + structured_output = result.output + else: + structured_output = result + + # Estimate tokens from prompt and response + response_text = "" + if hasattr(structured_output, "model_dump_json"): + response_text = structured_output.model_dump_json() + elif isinstance(structured_output, dict): + response_text = json.dumps(structured_output) + elif isinstance(structured_output, str): + response_text = structured_output + + # Extract usage information from the result + input_tokens = 0 + output_tokens = 0 + tool_calls_count = 0 + + if hasattr(result, "usage"): + usage = result.usage + input_tokens = getattr(usage, "input_tokens", 0) or 0 + output_tokens = getattr(usage, "output_tokens", 0) or 0 + tool_calls_count = getattr(usage, "tool_calls", 0) or 0 + + # Fallback to details field for certain models + if input_tokens == 0 and output_tokens == 0 and hasattr(usage, "details"): + details = usage.details + if isinstance(details, dict): + input_tokens = details.get("input_tokens", 0) or 0 + output_tokens = details.get("output_tokens", 0) or 0 + + # Calculate estimates with tool call support (always, for validation/fallback) + estimated = estimate_tokens_with_tools( + system_prompt=LINKED_ENTITIES_STRUCTURE_SYSTEM_PROMPT, + user_prompt=prompt, + response=response_text, + tool_calls=tool_calls_count, + tool_results_text=None, + ) + + # Use estimates as primary when API returns 0 + if input_tokens == 0 and output_tokens == 0: + logger.warning( + "API returned 0 tokens, using tiktoken estimates as primary counts", + ) + input_tokens = estimated.get("input_tokens", 0) + output_tokens = estimated.get("output_tokens", 0) + + usage_data = { + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "estimated_input_tokens": estimated.get("input_tokens", 0), + "estimated_output_tokens": estimated.get("output_tokens", 0), + } + + logger.info("Linked entities structuring completed successfully") + + return { + "data": structured_output, + "usage": usage_data, + } + + except Exception as e: + logger.error(f"Linked entities structuring failed: {e}", exc_info=True) + return { + "data": None, + "usage": None, + } diff --git a/src/agents/atomic_agents/models.py b/src/agents/atomic_agents/models.py new file mode 100644 index 0000000..413e4a2 --- /dev/null +++ b/src/agents/atomic_agents/models.py @@ -0,0 +1,321 @@ +""" +Simplified data models for atomic agents. + +These models use only primitive types (strings, numbers, lists, dicts) +to be compatible with LLM agents that don't support complex Pydantic types. +""" + +from typing import Any, Dict, List, Literal, Optional, get_args + +from pydantic import BaseModel, Field, field_validator + +# Import existing enums to avoid duplication +from ...data_models.models import Discipline, RepositoryType + + +class SimplifiedAuthor(BaseModel): + """Simplified author model with only primitive types.""" + + name: str + email: Optional[str] = None + orcid: Optional[str] = None + affiliations: List[str] = Field(default_factory=list) + + +class SimplifiedGitAuthor(BaseModel): + """Simplified git author model.""" + + name: str + email: Optional[str] = None + commits: Optional[Dict[str, Any]] = None + + +class SimplifiedRepositoryOutput(BaseModel): + """Simplified repository output model for structured output agent.""" + + name: Optional[str] = None + applicationCategory: Optional[List[str]] = None + codeRepository: Optional[List[str]] = None # URLs as strings + dateCreated: Optional[str] = None # ISO date string + license: Optional[str] = None + author: Optional[List[SimplifiedAuthor]] = None + gitAuthors: Optional[List[SimplifiedGitAuthor]] = None + discipline: Optional[List[str]] = None + disciplineJustification: Optional[List[str]] = None + repositoryType: str # Required + repositoryTypeJustification: List[str] # Required + + +class CompiledContext(BaseModel): + """Compiled context from the context compiler agent.""" + + markdown_content: str = Field( + description="Compiled markdown content with all repository information", + ) + repository_url: str = Field(description="Repository URL") + summary: Optional[str] = Field( + default=None, + description="Brief summary of the repository", + ) + + +class EPFLAssessment(BaseModel): + """EPFL relationship assessment.""" + + relatedToEPFL: bool = Field(description="Whether the repository is related to EPFL") + relatedToEPFLConfidence: float = Field( + ge=0.0, + le=1.0, + description="Confidence score (0.0 to 1.0) for EPFL relationship", + ) + relatedToEPFLJustification: str = Field( + description="Justification for EPFL relationship assessment", + ) + + +class EnrichedDataContext(BaseModel): + """Compiled enriched data context for EPFL final assessment.""" + + markdown_content: str = Field( + description="Compiled markdown content with all enriched repository information", + ) + repository_url: str = Field(description="Repository URL") + summary: Optional[str] = Field( + default=None, + description="Brief summary of enriched data", + ) + + +class LinkedEntitiesContext(BaseModel): + """Compiled academic catalog search results context.""" + + markdown_content: str = Field( + description="Compiled markdown content with search results from academic catalogs", + ) + repository_name: str = Field( + description="Repository or tool name that was searched", + ) + author_names: List[str] = Field( + description="List of author names that were searched", + default_factory=list, + ) + + +# Extract valid values from existing enums (avoiding duplication) +# Note: Literal types must be defined at module level for Pydantic schema generation. +# These values are manually synchronized with Discipline and RepositoryType enums +# from data_models.models to ensure they match exactly. + +ValidDiscipline = Literal[ + "Social sciences", + "Anthropology", + "Communication studies", + "Education", + "Linguistics", + "Research", + "Sociology", + "Geography", + "Psychology", + "Politics", + "Economics", + "Applied sciences", + "Health sciences", + "Electrical engineering", + "Chemical engineering", + "Civil engineering", + "Architecture", + "Computer engineering", + "Energy engineering", + "Military science", + "Industrial and production engineering", + "Mechanical engineering", + "Biological engineering", + "Environmental science", + "Systems science and engineering", + "Information engineering", + "Agricultural and food sciences", + "Business", + "Humanities", + "History", + "Literature", + "Art", + "Religion", + "Philosophy", + "Law", + "Formal sciences", + "Mathematics", + "Logic", + "Statistics", + "Theoretical computer science", + "Natural sciences", + "Physics", + "Astronomy", + "Biology", + "Chemistry", + "Earth science", +] + +ValidRepositoryType = Literal[ + "software", + "educational resource", + "documentation", + "data", + "webpage", + "other", +] + +# Runtime verification to ensure Literal values match enum values +_discipline_values = {d.value for d in Discipline} +_literal_discipline_values = get_args(ValidDiscipline) +assert set(_literal_discipline_values) == _discipline_values, ( + f"ValidDiscipline Literal values don't match Discipline enum values. " + f"Missing: {_discipline_values - set(_literal_discipline_values)}, " + f"Extra: {set(_literal_discipline_values) - _discipline_values}" +) + +_repo_type_values = {rt.value for rt in RepositoryType} +_literal_repo_type_values = get_args(ValidRepositoryType) +assert set(_literal_repo_type_values) == _repo_type_values, ( + f"ValidRepositoryType Literal values don't match RepositoryType enum values. " + f"Missing: {_repo_type_values - set(_literal_repo_type_values)}, " + f"Extra: {set(_literal_repo_type_values) - _repo_type_values}" +) + + +class RepositoryClassification(BaseModel): + """Repository type and discipline classification.""" + + repositoryType: ValidRepositoryType = Field( + description="Type of repository - must be one of the predefined types", + ) + repositoryTypeJustification: List[str] = Field( + description="List of justifications for the repository type classification", + default_factory=list, + ) + discipline: List[ValidDiscipline] = Field( + description="List of scientific disciplines - REQUIRED, must have at least one from the predefined list", + ) + disciplineJustification: List[str] = Field( + description="List of justifications for each discipline classification", + default_factory=list, + ) + + @field_validator("discipline") + @classmethod + def validate_discipline_not_empty(cls, v): + """Ensure at least one discipline is provided.""" + if not v or len(v) == 0: + raise ValueError( + "At least one discipline must be provided. Repository must belong to at least one scientific field.", + ) + return v + + +class SimplifiedOrganization(BaseModel): + """Simplified organization model for organization identifier agent.""" + + name: str = Field( + description="Name of the organization", + ) + organizationType: str = Field( + description="Type of organization (e.g., 'Research Institute', 'University', 'Company', 'Community Space', 'Non-Profit Organization', 'Government Agency', 'Software Project', 'Research Infrastructure') - REQUIRED", + ) + id: Optional[str] = Field( + default=None, + description="Organization identifier (GitHub URL, website, etc.)", + ) + attributionConfidence: Optional[float] = Field( + default=None, + description="Confidence score (0.0 to 1.0) for the organization's relationship to the repository", + ge=0.0, + le=1.0, + ) + + +class OrganizationIdentification(BaseModel): + """Identified organizations and their relationships to the repository.""" + + relatedToOrganizations: List[SimplifiedOrganization] = Field( + description="List of organizations related to this repository", + default_factory=list, + ) + relatedToOrganizationJustification: List[str] = Field( + description="List of justifications explaining how each organization is related to the repository", + default_factory=list, + ) + + +# Note: SimplifiedLinkedEntitiesRelation and SimplifiedLinkedEntitiesResult +# are now generated dynamically in linked_entities_searcher.py using create_simplified_model() +# to maintain consistency with other atomic agents + + +class SimplifiedGitHubUser(BaseModel): + """Simplified GitHubUser model for structured output agent.""" + + # Core identity (basic fields only - id and githubUserMetadata populated separately) + name: Optional[str] = None + fullname: Optional[str] = None + githubHandle: Optional[str] = None + + +class UserClassification(BaseModel): + """User discipline and position classification.""" + + discipline: List[ValidDiscipline] = Field( + description="List of scientific disciplines - at least one from the predefined list", + ) + disciplineJustification: List[str] = Field( + description="List of justifications for each discipline classification", + default_factory=list, + ) + position: List[str] = Field( + description="List of professional positions or roles", + default_factory=list, + ) + positionJustification: List[str] = Field( + description="List of justifications for each position", + default_factory=list, + ) + + @field_validator("discipline") + @classmethod + def validate_discipline_not_empty(cls, v): + """Ensure at least one discipline is provided.""" + if not v or len(v) == 0: + raise ValueError("At least one discipline must be provided.") + return v + + +class SimplifiedGitHubOrganization(BaseModel): + """Simplified GitHubOrganization model for structured output agent.""" + + # Core identity (basic fields only - id and githubOrganizationMetadata populated separately) + name: Optional[str] = None + description: Optional[str] = None + + +class OrganizationClassification(BaseModel): + """Organization type and discipline classification.""" + + organizationType: str = Field( + description="Type of organization (e.g., 'Research Institute', 'University', 'Company', 'Community Space', 'Non-Profit Organization', 'Government Agency', 'Software Project', 'Research Infrastructure')", + ) + organizationTypeJustification: str = Field( + description="Justification for the organization type classification", + ) + discipline: List[ValidDiscipline] = Field( + description="List of scientific disciplines - at least one from the predefined list", + ) + disciplineJustification: List[str] = Field( + description="List of justifications for each discipline classification", + default_factory=list, + ) + + @field_validator("discipline") + @classmethod + def validate_discipline_not_empty(cls, v): + """Ensure at least one discipline is provided.""" + if not v or len(v) == 0: + raise ValueError("At least one discipline must be provided.") + return v diff --git a/src/agents/atomic_agents/organization_classifier.py b/src/agents/atomic_agents/organization_classifier.py new file mode 100644 index 0000000..d7470e8 --- /dev/null +++ b/src/agents/atomic_agents/organization_classifier.py @@ -0,0 +1,176 @@ +""" +Organization classifier agent - Classifies organization type and discipline. + +This agent takes compiled context and classifies the organization's type +and discipline(s) with justifications. +""" + +import logging +from typing import Any, Dict + +from ...llm.model_config import load_model_config, validate_config +from ...utils.token_counter import estimate_tokens_from_messages +from ..agents_management import run_agent_with_fallback +from .models import CompiledContext, OrganizationClassification + +logger = logging.getLogger(__name__) + +# Load model configurations for organization classification +ORGANIZATION_CLASSIFIER_CONFIGS = load_model_config("run_organization_classifier") + +# Validate configurations +for config in ORGANIZATION_CLASSIFIER_CONFIGS: + if not validate_config(config): + logger.error(f"Invalid configuration for organization classifier: {config}") + raise ValueError("Invalid model configuration") + +# System prompt for organization classifier +ORGANIZATION_CLASSIFIER_SYSTEM_PROMPT = """ +You are an expert at classifying organizations by type and scientific discipline. + +Your task is to: +1. Analyze the compiled organization context provided +2. Determine the organization type (e.g., 'Research Institute', 'University', 'Company', 'Community Space', 'Non-Profit Organization', 'Government Agency', 'Software Project', 'Research Infrastructure') +3. Determine relevant scientific disciplines (AT LEAST ONE REQUIRED) from the allowed values in the schema +4. Provide clear justifications for each classification + +**Important Guidelines:** +- organizationType: REQUIRED - classify the organization type based on its structure, mission, and activities +- discipline: REQUIRED - must select at least one valid discipline from the schema enum +- If multiple disciplines apply, list all relevant ones +- Provide evidence-based justifications referencing specific information from the organization context +- Use the EXACT discipline names as specified in the JSON schema + +**Note:** Valid discipline values are enforced by the JSON schema enum constraints. +""" + + +def get_organization_classifier_prompt(compiled_context: CompiledContext) -> str: + """ + Generate prompt for organization classifier agent. + + Args: + compiled_context: Compiled organization context from context compiler + + Returns: + Formatted prompt string + """ + prompt = f"""Classify the following organization: + +**Organization Profile URL:** {compiled_context.repository_url} + +**Compiled Organization Context:** +{compiled_context.markdown_content} + +Please classify: +1. Organization Type (e.g., 'Research Institute', 'University', 'Company', 'Community Space', 'Non-Profit Organization', 'Government Agency', 'Software Project', 'Research Infrastructure') +2. Scientific Disciplines (one or more relevant fields from the allowed list) + +Provide clear justifications for each classification based on the organization context. +""" + + logger.debug(f"Organization classifier prompt length: {len(prompt)} chars") + return prompt + + +async def classify_organization_type_and_discipline( + compiled_context: CompiledContext, +) -> Dict[str, Any]: + """ + Classify organization type and discipline from compiled context. + + Args: + compiled_context: Compiled organization context from context compiler + + Returns: + Dictionary with: + - data: OrganizationClassification object + - usage: Token usage statistics + """ + logger.info("Starting organization classification...") + + # Generate prompt + prompt = get_organization_classifier_prompt(compiled_context) + + # Prepare agent context (minimal - just pass compiled context) + agent_context = { + "org_url": compiled_context.repository_url, + "compiled_context": compiled_context.markdown_content, + } + + # No tools needed for classification + tools = [] + + logger.debug(f"Prompt length: {len(prompt)} characters") + + # Run agent with schema enforcement + result = await run_agent_with_fallback( + ORGANIZATION_CLASSIFIER_CONFIGS, + prompt, + agent_context, + OrganizationClassification, # Schema enforcement + ORGANIZATION_CLASSIFIER_SYSTEM_PROMPT, + tools, + ) + + # Extract the classification from PydanticAI result + # Check if result has an .output attribute (PydanticAI wrapper) + if hasattr(result, "output"): + classification_data = result.output + else: + classification_data = result + + # Extract usage statistics from result attributes + usage_data = {} + input_tokens = 0 + output_tokens = 0 + + if hasattr(result, "usage"): + usage = result.usage + input_tokens = getattr(usage, "input_tokens", 0) or 0 + output_tokens = getattr(usage, "output_tokens", 0) or 0 + + # Fallback to details field for certain models + if input_tokens == 0 and output_tokens == 0 and hasattr(usage, "details"): + details = usage.details + if isinstance(details, dict): + input_tokens = details.get("input_tokens", 0) + output_tokens = details.get("output_tokens", 0) + + logger.info( + f"Organization classification usage: {input_tokens} input, {output_tokens} output tokens", + ) + else: + logger.warning("No usage data available from agent") + + usage_data["input_tokens"] = input_tokens + usage_data["output_tokens"] = output_tokens + + # Estimate tokens with tiktoken (serialize model properly) + response_text = "" + if hasattr(classification_data, "model_dump_json"): + response_text = classification_data.model_dump_json() + elif isinstance(classification_data, dict): + import json as json_module + + response_text = json_module.dumps(classification_data) + elif isinstance(classification_data, str): + response_text = classification_data + + estimated = estimate_tokens_from_messages( + system_prompt=ORGANIZATION_CLASSIFIER_SYSTEM_PROMPT, + user_prompt=prompt, + response=response_text, + ) + usage_data["estimated_input_tokens"] = estimated.get("input_tokens", 0) + usage_data["estimated_output_tokens"] = estimated.get("output_tokens", 0) + + logger.info( + f"Organization classified as '{classification_data.organizationType}' " + f"with {len(classification_data.discipline)} discipline(s)", + ) + + return { + "data": classification_data, + "usage": usage_data, + } diff --git a/src/agents/atomic_agents/organization_context_compiler.py b/src/agents/atomic_agents/organization_context_compiler.py new file mode 100644 index 0000000..6b35ffd --- /dev/null +++ b/src/agents/atomic_agents/organization_context_compiler.py @@ -0,0 +1,369 @@ +""" +Organization context compiler agent - First stage of atomic agent pipeline. + +This agent uses tools to gather comprehensive organization information +and compile it into a markdown document for the next agent. +""" + +import json +import logging +from typing import Any, Dict + +import httpx + +from ...context.infoscience import ( + get_author_publications_tool, + search_infoscience_labs_tool, + search_infoscience_publications_tool, +) +from ...llm.model_config import load_model_config, validate_config +from ...utils.token_counter import ( + estimate_tokens_with_tools, +) +from ..agents_management import run_agent_with_fallback +from .models import CompiledContext + +logger = logging.getLogger(__name__) + +# Load model configurations for organization context compilation +ORGANIZATION_CONTEXT_COMPILER_CONFIGS = load_model_config( + "run_organization_context_compiler", +) + +# Validate configurations +for config in ORGANIZATION_CONTEXT_COMPILER_CONFIGS: + if not validate_config(config): + logger.error( + f"Invalid configuration for organization context compiler: {config}", + ) + raise ValueError("Invalid model configuration") + +# System prompt for organization context compiler +ORGANIZATION_CONTEXT_COMPILER_SYSTEM_PROMPT = """ +You are an expert at gathering and compiling comprehensive information about GitHub organizations and research institutions. + +Your task is to: +1. Analyze the GitHub organization metadata provided (description, location, members, repositories, etc.) +2. Extract organization name variations and aliases from the metadata: + - Full names (e.g., "Swiss Data Science Center") + - Short names or acronyms (e.g., "SDSC") + - GitHub handles (e.g., "sdsc-ordes") + - Any alternative names mentioned in description, README, or metadata +3. Use available tools to search for additional information: + - **Search Infoscience with MULTIPLE name variations**: Try the organization name, full name, acronyms, and any aliases found in the context + - Search Infoscience for EPFL labs and organizational units (orgunit) matching the organization + - Search Infoscience for publications related to the organization (try different name variations) + - Get publications by organization members from Infoscience + - Search the web for additional context about the organization (try different name variations) +4. Compile all information into a well-structured markdown document + +**Search Strategy:** +- **ALWAYS try multiple name variations** when searching Infoscience and web: + - Start with the GitHub organization name/handle + - Try the full organization name if different (e.g., from description) + - Try acronyms or short names (e.g., "SDSC" if full name is "Swiss Data Science Center") + - Try name variations found in the metadata (description, README, etc.) + - If one search returns no results, try another variation +- Example: For "sdsc-ordes", also search for "SDSC", "Swiss Data Science Center", "Swiss Data Science Center - ORDES", etc. + +**Input Sources:** +- GitHub organization metadata: Description, location, members, repositories, README, etc. +- Tool results: Infoscience lab/orgunit searches, publication searches, web search results + +**Output Format:** +Return ONLY a comprehensive markdown document (plain text, not JSON) that includes: +- Organization overview and mission +- Organization type and structure +- Affiliations and relationships (from GitHub, Infoscience) +- Research interests and disciplines (inferred from description, repositories, publications) +- Publications and research outputs (if found) +- Any other relevant information from GitHub metadata and tool searches + +The compiled context should be thorough and well-organized for the next agent to extract structured metadata. + +**IMPORTANT:** Return ONLY the markdown document as plain text. Do not wrap it in JSON, do not add any explanatory text, do not use code blocks. Just return the raw markdown text. +""" + + +async def search_web_tool( + query: str, +) -> str: + """ + Search DuckDuckGo for information about an organization. + + Args: + query: The search query about an organization (e.g., "Swiss Data Science Center EPFL") + + Returns: + Summary of search results from DuckDuckGo (JSON string) + """ + logger.info(f"🔍 Agent tool called: search_web_tool('{query}')") + + try: + # Simple DuckDuckGo search using their instant answer API + async with httpx.AsyncClient() as client: + response = await client.get( + "https://api.duckduckgo.com/", + params={ + "q": query, + "format": "json", + "no_html": "1", + "skip_disambig": "1", + }, + timeout=10.0, + ) + response.raise_for_status() + data = response.json() + + results = [] + # Extract abstract if available + if data.get("Abstract"): + results.append( + { + "title": data.get("Heading", ""), + "abstract": data.get("Abstract", ""), + "url": data.get("AbstractURL", ""), + }, + ) + + # Extract related topics + for topic in data.get("RelatedTopics", [])[:5]: + if isinstance(topic, dict) and "Text" in topic: + results.append( + { + "title": topic.get("Text", ""), + "url": topic.get("FirstURL", ""), + }, + ) + + logger.info( + f"✓ Web search for '{query}' returned {len(results)} results", + ) + return json.dumps( + { + "query": query, + "results": results, + }, + indent=2, + ) + + except Exception as e: + logger.error(f"✗ Error searching web for '{query}': {e}") + return json.dumps({"error": str(e)}) + + +def get_organization_context_compiler_prompt( + org_name: str, + org_url: str, + github_metadata: Dict[str, Any], +) -> str: + """ + Generate prompt for organization context compiler agent. + + Args: + org_name: GitHub organization name + org_url: GitHub organization profile URL + github_metadata: GitHub organization metadata dict + + Returns: + Formatted prompt string + """ + prompt = f"""Compile comprehensive information about this GitHub organization: + +**Organization Name:** {org_name} +**GitHub Organization URL:** {org_url} + +**GitHub Organization Metadata:** +{json.dumps(github_metadata, indent=2, default=str)} +""" + + prompt += """ + +Please: +1. Analyze the GitHub organization metadata provided +2. **Extract organization name variations** from the metadata: + - Look for full names, acronyms, short names, or aliases in the description, README, or other metadata fields + - Note any alternative names or variations that might be used in academic databases +3. Use available tools to search for additional information: + - **Search Infoscience with MULTIPLE name variations**: + * Start with the organization name: "{org_name}" + * Also try the full organization name if different (e.g., from description field) + * Try acronyms or short names (e.g., if description mentions "SDSC", also search for "SDSC") + * Try any alternative names or variations found in the metadata + * Search for EPFL labs/organizational units (orgunit search) with each variation + * Search for publications related to the organization with each variation + - Get publications by organization members from Infoscience if members are listed + - Search the web for additional context about the organization (try different name variations) +4. Compile all information into a comprehensive markdown document + +**Search Strategy:** +- If a search with one name returns no results, try another variation +- Example: For "{org_name}", if the description mentions "Swiss Data Science Center", also search for: + * "Swiss Data Science Center" + * "SDSC" (if that's the acronym) + * Any other variations found in the metadata + +Focus on gathering information that will help extract: +- Organization type and structure +- Research interests and scientific disciplines +- Affiliations and relationships with other organizations +- Publications and research outputs +- Mission and purpose + +**IMPORTANT:** Return ONLY the markdown document as plain text. Do not wrap it in JSON, do not add any explanatory text, do not use code blocks. Just return the raw markdown text. +""" + + logger.debug(f"Organization context compiler prompt length: {len(prompt)} chars") + return prompt + + +async def compile_organization_context( + org_name: str, + org_url: str, + github_metadata: Dict[str, Any], +) -> Dict[str, Any]: + """ + Compile organization context using tools to gather comprehensive information. + + Args: + org_name: GitHub organization name + org_url: GitHub organization profile URL + github_metadata: GitHub organization metadata dict + + Returns: + Dictionary with 'data' (CompiledContext) and 'usage' (dict with token info) + """ + # Create context for the agent + agent_context = { + "org_name": org_name, + "org_url": org_url, + "github_metadata": github_metadata, + } + + # Prepare the prompt + prompt = get_organization_context_compiler_prompt( + org_name, + org_url, + github_metadata, + ) + + # Define tools for the organization context compiler + tools = [ + search_infoscience_labs_tool, + search_infoscience_publications_tool, + get_author_publications_tool, + search_web_tool, + ] + + try: + # Run agent with fallback across multiple models + # Use str as output type - agent returns markdown text + result = await run_agent_with_fallback( + ORGANIZATION_CONTEXT_COMPILER_CONFIGS, + prompt, + agent_context, + str, # Simple string output - just markdown text + ORGANIZATION_CONTEXT_COMPILER_SYSTEM_PROMPT, + tools, + ) + + # Extract the markdown string from PydanticAI result + if hasattr(result, "output"): + markdown_content = result.output + else: + markdown_content = result + + # Convert string to CompiledContext + if isinstance(markdown_content, str): + compiled_context = CompiledContext( + markdown_content=markdown_content, + repository_url=org_url, # Use org_url as repository_url for consistency + summary=None, + ) + else: + # Fallback if we get something unexpected + compiled_context = CompiledContext( + markdown_content=str(markdown_content), + repository_url=org_url, + summary=None, + ) + + # Estimate tokens from prompt and response + response_text = "" + if hasattr(compiled_context, "model_dump_json"): + response_text = compiled_context.model_dump_json() + elif isinstance(compiled_context, dict): + import json as json_module + + response_text = json_module.dumps(compiled_context) + elif isinstance(compiled_context, str): + response_text = compiled_context + + # Extract usage information from the result + input_tokens = 0 + output_tokens = 0 + tool_calls_count = 0 + + if hasattr(result, "usage"): + usage = result.usage + input_tokens = getattr(usage, "input_tokens", 0) or 0 + output_tokens = getattr(usage, "output_tokens", 0) or 0 + tool_calls_count = getattr(usage, "tool_calls", 0) or 0 + + # Fallback to details field for certain models + if input_tokens == 0 and output_tokens == 0 and hasattr(usage, "details"): + details = usage.details + if isinstance(details, dict): + input_tokens = details.get("input_tokens", 0) or 0 + output_tokens = details.get("output_tokens", 0) or 0 + + # Calculate estimates with tool call support (always, for validation/fallback) + estimated = estimate_tokens_with_tools( + system_prompt=ORGANIZATION_CONTEXT_COMPILER_SYSTEM_PROMPT, + user_prompt=prompt, + response=response_text, + tool_calls=tool_calls_count, + tool_results_text=None, + ) + + # Use estimates as primary when API returns 0 + if input_tokens == 0 and output_tokens == 0: + logger.warning( + "API returned 0 tokens, using tiktoken estimates as primary counts", + ) + input_tokens = estimated.get("input_tokens", 0) + output_tokens = estimated.get("output_tokens", 0) + + usage_data = { + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "estimated_input_tokens": estimated.get("input_tokens", 0), + "estimated_output_tokens": estimated.get("output_tokens", 0), + } + + # Log compiled context size + if hasattr(compiled_context, "markdown_content"): + content_size = len(compiled_context.markdown_content) + elif ( + isinstance(compiled_context, dict) + and "markdown_content" in compiled_context + ): + content_size = len(compiled_context.get("markdown_content", "")) + else: + content_size = 0 + + logger.info( + f"Organization context compilation completed: {content_size:,} chars of markdown", + ) + + return { + "data": compiled_context, + "usage": usage_data, + } + + except Exception as e: + logger.error(f"Organization context compilation failed: {e}", exc_info=True) + return { + "data": None, + "usage": None, + } diff --git a/src/agents/atomic_agents/organization_identifier.py b/src/agents/atomic_agents/organization_identifier.py new file mode 100644 index 0000000..bccf0d9 --- /dev/null +++ b/src/agents/atomic_agents/organization_identifier.py @@ -0,0 +1,283 @@ +""" +Organization Identifier Agent - Atomic agent for identifying related organizations. + +This agent analyzes compiled repository context and identifies organizations +that are related to the repository (developers, maintainers, sponsors, etc.). +""" + +import logging +from typing import Any, Dict + +from ...llm.model_config import load_model_config, validate_config +from ...utils.token_counter import estimate_tokens_from_messages +from ..agents_management import run_agent_with_fallback +from .models import CompiledContext, OrganizationIdentification + +logger = logging.getLogger(__name__) + +# Load model configurations for organization identification +ORGANIZATION_IDENTIFIER_CONFIGS = load_model_config("run_organization_identifier") + +# Validate configurations +for config in ORGANIZATION_IDENTIFIER_CONFIGS: + if not validate_config(config): + logger.error(f"Invalid configuration for organization identifier: {config}") + raise ValueError("Invalid model configuration") + +# System prompt for organization identifier (generic - works for both repositories and users) +ORGANIZATION_IDENTIFIER_SYSTEM_PROMPT = """ +You are an expert at identifying institutional organizations related to software repositories or users. + +Your task is to: +1. Analyze the compiled context provided (repository or user context). +2. Identify institutional organizations that are related to the repository or user. +3. Determine the type of each organization (Research Institute, University, Company, Community Space, etc.) - **REQUIRED for each organization**. +4. Provide a confidence score (0.0 to 1.0) indicating how confident you are about the organization's relationship. +5. Provide clear justifications explaining how each organization is related. + +**For Repositories:** +- Focus on organizations that have a DIRECT institutional relationship with the software itself +- A side affiliation of an author is NOT enough - the organization must be directly related to the software +- Examples: developers, maintainers, sponsors, hosts, institutional partners, research groups/labs directly associated with the software + +**For Users:** +- Focus on organizations that the user is affiliated with +- Examples: current or past employers, universities, research institutes, labs, companies, organizations mentioned in bio/ORCID/GitHub profile + +**For Organizations:** +- Focus on organizations that are related to this organization +- Examples: parent organizations, partner organizations, affiliated organizations, founding organizations, collaborating institutions + +**Organization Types (REQUIRED for each organization):** +- Research Institute +- University +- Government Agency +- Private Company +- Non-Profit Organization +- Community Space +- Software Project +- Research Infrastructure +- etc. + +**Important:** +- Extract organization names from the provided context (README, documentation, bio, ORCID, GitHub, etc.) +- Look for GitHub organization URLs (e.g., https://github.com/orgname) +- Check for explicit mentions in documentation, funding sections, acknowledgments, bio, ORCID records +- Provide specific evidence-based justifications that demonstrate the relationship +- Each justification should reference specific evidence from the context + +**Output Format:** +Return a JSON object matching the OrganizationIdentification schema exactly. The schema requires: +- relatedToOrganizations: List of SimplifiedOrganization objects, each with: + - name: Organization name (REQUIRED) + - organizationType: Type of organization (REQUIRED - must be a string like "Research Institute", "University", etc.) + - id: Optional organization identifier (GitHub URL, website, etc.) + - attributionConfidence: Optional confidence score (0.0 to 1.0) +- relatedToOrganizationJustification: List of justification strings (one per organization) +""" + + +def get_organization_identifier_prompt( + compiled_context: CompiledContext, + context_type: str = "repository", +) -> str: + """ + Generate prompt for organization identifier agent. + + Args: + compiled_context: Compiled context from context compiler + context_type: Type of context - "repository", "user", or "organization" + + Returns: + Formatted prompt string + """ + if context_type == "organization": + url_label = "Organization Profile URL" + elif context_type == "user": + url_label = "User Profile URL" + else: + url_label = "Repository URL" + + if context_type == "organization": + prompt = f"""Identify institutional organizations that are related to this organization: + +**{url_label}:** {compiled_context.repository_url} + +**Compiled Organization Context:** +{compiled_context.markdown_content} + +**IMPORTANT:** Identify organizations that are related to this organization, such as: +- Parent organizations (e.g., a lab's parent university) +- Partner organizations (collaborating institutions) +- Affiliated organizations (organizations this org is part of or works with) +- Founding organizations (if this org was established by other orgs) +- Organizations mentioned in the organization's description, README, or metadata + +Please identify: +1. Institutional organizations related to this organization +2. The type of each organization (REQUIRED - e.g., 'Research Institute', 'University', 'Company', etc.) +3. Organization identifiers (GitHub URLs, websites, ROR IDs, etc.) +4. Confidence score (0.0 to 1.0) for each organization's relationship + +For each organization, provide: +- The organization type (REQUIRED) +- A confidence score indicating how certain you are about the relationship +- Clear justifications that demonstrate the relationship, referencing specific evidence from the organization context +""" + elif context_type == "user": + prompt = f"""Identify institutional organizations that this user is affiliated with: + +**{url_label}:** {compiled_context.repository_url} + +**Compiled User Context:** +{compiled_context.markdown_content} + +**IMPORTANT:** Identify organizations that the user is affiliated with, such as: +- Current or past employers +- Universities or educational institutions +- Research institutes or labs +- Companies or organizations they work for +- Organizations mentioned in their bio, ORCID, or GitHub profile + +Please identify: +1. Institutional organizations the user is affiliated with +2. The type of each organization (REQUIRED - e.g., 'Research Institute', 'University', 'Company', etc.) +3. Organization identifiers (GitHub URLs, websites, ROR IDs, etc.) +4. Confidence score (0.0 to 1.0) for each organization's relationship to the user + +For each organization, provide: +- The organization type (REQUIRED) +- A confidence score indicating how certain you are about the affiliation +- Clear justifications that demonstrate the user's affiliation, referencing specific evidence from the user context +""" + else: + prompt = f"""Identify institutional organizations DIRECTLY related to the following software repository: + +**{url_label}:** {compiled_context.repository_url} + +**Compiled Repository Context:** +{compiled_context.markdown_content} + +**IMPORTANT:** Only identify organizations that have a DIRECT institutional relationship with the software itself. A side affiliation of an author is NOT sufficient - the organization must be directly involved with the software development, funding, hosting, or partnership. + +Please identify: +1. Institutional organizations DIRECTLY related to this software (developers, maintainers, sponsors, hosts, institutional partners) +2. The type of each organization (REQUIRED - e.g., 'Research Institute', 'University', 'Company', etc.) +3. Organization identifiers (GitHub URLs, websites, etc.) +4. Confidence score (0.0 to 1.0) for each organization's relationship to the repository + +For each organization, provide: +- The organization type (REQUIRED) +- A confidence score indicating how certain you are about the relationship +- Clear justifications that demonstrate the DIRECT relationship with the software, referencing specific evidence from the repository context +""" + + logger.debug(f"Organization identifier prompt length: {len(prompt)} chars") + return prompt + + +async def identify_related_organizations( + compiled_context: CompiledContext, + context_type: str = "repository", +) -> Dict[str, Any]: + """ + Identify organizations related to the repository or user using an atomic agent. + + Args: + compiled_context: Compiled markdown content with all repository/user/organization information + context_type: Type of context - "repository", "user", or "organization" (default: "repository") + + Returns: + Dictionary with 'data' (OrganizationIdentification) and 'usage' (dict with token info) + """ + logger.info( + f"Identifying related organizations for {compiled_context.repository_url} (context_type: {context_type})", + ) + + # Prepare the prompt + prompt = get_organization_identifier_prompt( + compiled_context, + context_type=context_type, + ) + + # Create agent context + agent_context = { + "repository_url": compiled_context.repository_url, + "compiled_context": compiled_context.markdown_content, + } + + # No tools needed for identification + tools = [] + + logger.debug(f"Prompt length: {len(prompt)} characters") + + # Run agent with schema enforcement + result = await run_agent_with_fallback( + ORGANIZATION_IDENTIFIER_CONFIGS, + prompt, + agent_context, + OrganizationIdentification, # Schema enforcement + ORGANIZATION_IDENTIFIER_SYSTEM_PROMPT, + tools, + ) + + # Extract the identification from PydanticAI result + # Check if result has an .output attribute (PydanticAI wrapper) + if hasattr(result, "output"): + identification_data = result.output + else: + identification_data = result + + # Extract usage statistics from result attributes + usage_data = {} + input_tokens = 0 + output_tokens = 0 + + if hasattr(result, "usage"): + usage = result.usage + input_tokens = getattr(usage, "input_tokens", 0) or 0 + output_tokens = getattr(usage, "output_tokens", 0) or 0 + + # Fallback to details field for certain models + if input_tokens == 0 and output_tokens == 0 and hasattr(usage, "details"): + details = usage.details + if isinstance(details, dict): + input_tokens = details.get("input_tokens", 0) + output_tokens = details.get("output_tokens", 0) + + logger.info( + f"Organization identification usage: {input_tokens} input, {output_tokens} output tokens", + ) + else: + logger.warning("No usage data available from agent") + + usage_data["input_tokens"] = input_tokens + usage_data["output_tokens"] = output_tokens + + # Estimate tokens with tiktoken (serialize model properly) + response_text = "" + if hasattr(identification_data, "model_dump_json"): + response_text = identification_data.model_dump_json() + elif isinstance(identification_data, dict): + import json as json_module + + response_text = json_module.dumps(identification_data) + elif isinstance(identification_data, str): + response_text = identification_data + + estimated = estimate_tokens_from_messages( + system_prompt=ORGANIZATION_IDENTIFIER_SYSTEM_PROMPT, + user_prompt=prompt, + response=response_text, + ) + usage_data["estimated_input_tokens"] = estimated.get("input_tokens", 0) + usage_data["estimated_output_tokens"] = estimated.get("output_tokens", 0) + + logger.info( + f"Identified {len(identification_data.relatedToOrganizations)} related organizations", + ) + + return { + "data": identification_data, + "usage": usage_data, + } diff --git a/src/agents/atomic_agents/organization_structured_output.py b/src/agents/atomic_agents/organization_structured_output.py new file mode 100644 index 0000000..888fe92 --- /dev/null +++ b/src/agents/atomic_agents/organization_structured_output.py @@ -0,0 +1,245 @@ +""" +Organization structured output agent - Second stage of atomic agent pipeline. + +This agent takes compiled context and simplified schema instructions +to produce structured metadata output for organizations. +""" + +import json +import logging +from typing import Any, Dict, Optional + +from ...data_models.conversion import create_simplified_model +from ...data_models.organization import GitHubOrganization +from ...llm.model_config import load_model_config, validate_config +from ...utils.token_counter import ( + estimate_tokens_with_tools, +) +from ..agents_management import run_agent_with_fallback +from .models import CompiledContext + +logger = logging.getLogger(__name__) + +# Configuration: Fields that should be extracted by the LLM model +# Only basic identity fields - enrichment fields handled by specialized agents +MODEL_EXTRACTION_FIELDS = [ + "name", + "description", +] + +# Load model configurations for organization structured output +ORGANIZATION_STRUCTURED_OUTPUT_CONFIGS = load_model_config( + "run_organization_structured_output", +) + +# Validate configurations +for config in ORGANIZATION_STRUCTURED_OUTPUT_CONFIGS: + if not validate_config(config): + logger.error( + f"Invalid configuration for organization structured output: {config}", + ) + raise ValueError("Invalid model configuration") + +# Generate simplified model dynamically from GitHubOrganization +# Only include fields that should be extracted by the model +# Cache it at module level to avoid regenerating on every call +_SIMPLIFIED_MODEL, _UNION_METADATA = create_simplified_model( + GitHubOrganization, + field_filter=MODEL_EXTRACTION_FIELDS, +) + +# System prompt for organization structured output agent +ORGANIZATION_STRUCTURED_OUTPUT_SYSTEM_PROMPT = """ +You are an expert at extracting structured metadata from organization information. + +Your task is to: +1. Analyze the compiled organization context provided +2. Extract basic identity fields according to the simplified schema provided +3. Output only the specified fields with correct data types + +**Important Constraints:** +- Use ONLY primitive types: strings, numbers, lists, and dictionaries +- URLs must be strings (not HttpUrl objects) +- Do not include fields not in the schema +- All required fields must be present + +**Output Format:** +Return a JSON object matching the provided schema exactly. +""" + + +def get_organization_structured_output_prompt( + compiled_context: CompiledContext, + schema: Dict[str, Any], + example: Optional[Dict[str, Any]] = None, +) -> str: + """ + Generate prompt for organization structured output agent. + + Args: + compiled_context: Compiled context from first agent + schema: Simplified schema definition + example: Optional example output + + Returns: + Formatted prompt string + """ + prompt = f"""Extract basic identity metadata from the compiled organization context. + +**Compiled Context:** +{compiled_context.markdown_content} + +**Organization Profile URL:** {compiled_context.repository_url} + +**Expected Output Schema:** +{json.dumps(schema, indent=2)} +""" + + if example: + prompt += f""" + +**Example Output (for reference):** +{json.dumps(example, indent=2)} +""" + + prompt += """ + +Please extract and return basic identity fields matching the schema exactly. +Use only primitive types (strings, numbers, lists, dicts). +""" + + return prompt + + +async def generate_organization_structured_output( + compiled_context: CompiledContext, + schema: Dict[str, Any], + example: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + """ + Generate structured output from compiled organization context. + + Args: + compiled_context: Compiled context from organization context compiler + schema: Simplified schema definition + example: Optional example output + + Returns: + Dictionary with 'data' (dynamically generated simplified model), 'usage' (dict with token info), + and 'union_metadata' (dict for Union field reconciliation) + """ + # Create context for the agent + agent_context = { + "compiled_context": compiled_context, + "schema": schema, + } + + # Prepare the prompt + prompt = get_organization_structured_output_prompt( + compiled_context, + schema, + example, + ) + + # No tools for structured output agent + tools = [] + + try: + # Run agent with fallback across multiple models + # Use dynamically generated simplified model + result = await run_agent_with_fallback( + ORGANIZATION_STRUCTURED_OUTPUT_CONFIGS, + prompt, + agent_context, + _SIMPLIFIED_MODEL, + ORGANIZATION_STRUCTURED_OUTPUT_SYSTEM_PROMPT, + tools, # No tools for this agent + ) + + # Extract the output from PydanticAI result + if hasattr(result, "output"): + structured_output = result.output + else: + structured_output = result + + # Estimate tokens from prompt and response + response_text = "" + if hasattr(structured_output, "model_dump_json"): + response_text = structured_output.model_dump_json() + elif isinstance(structured_output, dict): + import json as json_module + + response_text = json_module.dumps(structured_output) + elif isinstance(structured_output, str): + response_text = structured_output + + # Extract usage information from the result + input_tokens = 0 + output_tokens = 0 + tool_calls_count = 0 + + if hasattr(result, "usage"): + usage = result.usage + input_tokens = getattr(usage, "input_tokens", 0) or 0 + output_tokens = getattr(usage, "output_tokens", 0) or 0 + tool_calls_count = getattr(usage, "tool_calls", 0) or 0 + + # Fallback to details field for certain models + if input_tokens == 0 and output_tokens == 0 and hasattr(usage, "details"): + details = usage.details + if isinstance(details, dict): + input_tokens = details.get("input_tokens", 0) or 0 + output_tokens = details.get("output_tokens", 0) or 0 + + # Calculate estimates with tool call support (always, for validation/fallback) + estimated = estimate_tokens_with_tools( + system_prompt=ORGANIZATION_STRUCTURED_OUTPUT_SYSTEM_PROMPT, + user_prompt=prompt, + response=response_text, + tool_calls=tool_calls_count, + tool_results_text=None, + ) + + # Use estimates as primary when API returns 0 + if input_tokens == 0 and output_tokens == 0: + logger.warning( + "API returned 0 tokens, using tiktoken estimates as primary counts", + ) + input_tokens = estimated.get("input_tokens", 0) + output_tokens = estimated.get("output_tokens", 0) + + usage_data = { + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "estimated_input_tokens": estimated.get("input_tokens", 0), + "estimated_output_tokens": estimated.get("output_tokens", 0), + } + + # Log output summary + if hasattr(structured_output, "model_dump"): + output_dict = structured_output.model_dump() + elif isinstance(structured_output, dict): + output_dict = structured_output + else: + output_dict = {} + + logger.info( + f"Organization structured output generated: {len(output_dict)} top-level fields", + ) + + return { + "data": structured_output, + "usage": usage_data, + "union_metadata": _UNION_METADATA, + } + + except Exception as e: + logger.error( + f"Organization structured output generation failed: {e}", + exc_info=True, + ) + return { + "data": None, + "usage": None, + "union_metadata": _UNION_METADATA, + } diff --git a/src/agents/atomic_agents/repository_classifier.py b/src/agents/atomic_agents/repository_classifier.py new file mode 100644 index 0000000..56f5afa --- /dev/null +++ b/src/agents/atomic_agents/repository_classifier.py @@ -0,0 +1,184 @@ +""" +Repository classifier agent - Classifies repository type and discipline. + +This agent takes compiled context and classifies the repository's type +and scientific discipline(s) with justifications. +""" + +import logging +from typing import Any, Dict + +from ...llm.model_config import load_model_config, validate_config +from ...utils.token_counter import estimate_tokens_from_messages +from ..agents_management import run_agent_with_fallback +from .models import CompiledContext, RepositoryClassification + +logger = logging.getLogger(__name__) + +# Load model configurations for repository classification +REPOSITORY_CLASSIFIER_CONFIGS = load_model_config("run_repository_classifier") + +# Validate configurations +for config in REPOSITORY_CLASSIFIER_CONFIGS: + if not validate_config(config): + logger.error(f"Invalid configuration for repository classifier: {config}") + raise ValueError("Invalid model configuration") + +# System prompt for repository classifier +REPOSITORY_CLASSIFIER_SYSTEM_PROMPT = """ +You are an expert at classifying software repositories by type and scientific discipline. + +Your task is to: +1. Analyze the compiled repository context provided +2. Determine the primary repository type from the allowed values in the schema +3. Identify relevant scientific disciplines (AT LEAST ONE REQUIRED) from the allowed values in the schema +4. Provide clear justifications for each classification + +**Important Guidelines:** +- repositoryType: Choose from the valid types in the schema (software, educational resource, documentation, data, webpage, other) +- discipline: REQUIRED - must select at least one valid discipline from the schema enum +- If multiple disciplines apply, list all relevant ones +- Provide evidence-based justifications referencing specific repository content +- Use the EXACT discipline names as specified in the JSON schema + +**Repository Type Descriptions:** +- software: Code for applications, tools, libraries, frameworks +- educational resource: Educational materials, courses, tutorials +- documentation: Primarily documentation, guides +- data: Data collections, databases, datasets +- webpage: Static websites, landing pages, personal pages +- other: Anything that doesn't fit the above categories + +**Note:** Valid discipline and repository type values are enforced by the JSON schema enum constraints. +""" + + +def get_repository_classifier_prompt(compiled_context: CompiledContext) -> str: + """ + Generate prompt for repository classifier agent. + + Args: + compiled_context: Compiled repository context from context compiler + + Returns: + Formatted prompt string + """ + prompt = f"""Classify the following repository: + +**Repository URL:** {compiled_context.repository_url} + +**Compiled Repository Context:** +{compiled_context.markdown_content} + +Please classify: +1. Repository Type (software, dataset, model, documentation, or other) +2. Scientific Disciplines (one or more relevant fields) + +Provide clear justifications for each classification based on the repository content. +""" + + logger.debug(f"Repository classifier prompt length: {len(prompt)} chars") + return prompt + + +async def classify_repository_type_and_discipline( + compiled_context: CompiledContext, +) -> Dict[str, Any]: + """ + Classify repository type and discipline from compiled context. + + Args: + compiled_context: Compiled repository context from context compiler + + Returns: + Dictionary with: + - data: RepositoryClassification object + - usage: Token usage statistics + """ + logger.info("Starting repository classification...") + + # Generate prompt + prompt = get_repository_classifier_prompt(compiled_context) + + # Prepare agent context (minimal - just pass compiled context) + agent_context = { + "repository_url": compiled_context.repository_url, + "compiled_context": compiled_context.markdown_content, + } + + # No tools needed for classification + tools = [] + + logger.debug(f"Prompt length: {len(prompt)} characters") + + # Run agent with schema enforcement + result = await run_agent_with_fallback( + REPOSITORY_CLASSIFIER_CONFIGS, + prompt, + agent_context, + RepositoryClassification, # Schema enforcement + REPOSITORY_CLASSIFIER_SYSTEM_PROMPT, + tools, + ) + + # Extract the classification from PydanticAI result + # Check if result has an .output attribute (PydanticAI wrapper) + if hasattr(result, "output"): + classification_data = result.output + else: + classification_data = result + + # Extract usage statistics from result attributes + usage_data = {} + input_tokens = 0 + output_tokens = 0 + + if hasattr(result, "usage"): + usage = result.usage + input_tokens = getattr(usage, "input_tokens", 0) or 0 + output_tokens = getattr(usage, "output_tokens", 0) or 0 + + # Fallback to details field for certain models + if input_tokens == 0 and output_tokens == 0 and hasattr(usage, "details"): + details = usage.details + if isinstance(details, dict): + input_tokens = details.get("input_tokens", 0) + output_tokens = details.get("output_tokens", 0) + + logger.info( + f"Repository classification usage: {input_tokens} input, {output_tokens} output tokens", + ) + else: + logger.warning("No usage data available from agent") + + usage_data["input_tokens"] = input_tokens + usage_data["output_tokens"] = output_tokens + + # Estimate tokens with tiktoken (serialize model properly) + response_text = "" + if hasattr(classification_data, "model_dump_json"): + response_text = classification_data.model_dump_json() + elif isinstance(classification_data, dict): + import json as json_module + + response_text = json_module.dumps(classification_data) + elif isinstance(classification_data, str): + response_text = classification_data + + estimated = estimate_tokens_from_messages( + system_prompt=REPOSITORY_CLASSIFIER_SYSTEM_PROMPT, + user_prompt=prompt, + response=response_text, + ) + usage_data["estimated_input_tokens"] = estimated.get("input_tokens", 0) + usage_data["estimated_output_tokens"] = estimated.get("output_tokens", 0) + + logger.info( + f"Repository classified as: {classification_data.repositoryType} " + f"with {len(classification_data.discipline)} discipline(s)", + ) + + return { + "data": classification_data, + "usage": usage_data, + } diff --git a/src/agents/atomic_agents/structured_output.py b/src/agents/atomic_agents/structured_output.py new file mode 100644 index 0000000..95ee29f --- /dev/null +++ b/src/agents/atomic_agents/structured_output.py @@ -0,0 +1,270 @@ +""" +Structured output agent - Second stage of atomic agent pipeline. + +This agent takes compiled context and simplified schema instructions +to produce structured metadata output. +""" + +import json +import logging +from typing import Any, Dict, Optional + +from ...data_models.conversion import create_simplified_model +from ...data_models.repository import SoftwareSourceCode +from ...llm.model_config import load_model_config, validate_config +from ...utils.token_counter import ( + estimate_tokens_with_tools, +) +from ..agents_management import run_agent_with_fallback +from .models import CompiledContext + +logger = logging.getLogger(__name__) + +# Configuration: Fields that should be extracted by the LLM model +# Fields NOT in this list will be automatically populated from GIMIE/git data +# and merged with model output +MODEL_EXTRACTION_FIELDS = [ + # Core metadata (requires LLM analysis) + # "name", + "description", + "applicationCategory", + "featureList", + # Classification (handled by Stage 3: Repository Classifier) + # "discipline", # Removed - handled by repository_classifier.py + # "disciplineJustification", # Removed - handled by repository_classifier.py + # "repositoryType", # Removed - handled by repository_classifier.py + # "repositoryTypeJustification", # Removed - handled by repository_classifier.py + # Authors (requires LLM analysis to identify from README, etc.) + "author", # Will be split into authorPerson and authorOrganization + # Related entities (handled by Stage 4: Organization Identifier) + # "relatedToOrganizations", # Removed - handled by organization_identifier.py + # "relatedToOrganizationJustification", # Removed - handled by organization_identifier.py + "relatedDatasets", + "relatedPublications", + "relatedModels", + "relatedAPIs", + # EPFL assessment (handled separately by EPFL checker agent) + # Note: relatedToEPFL fields are handled by EPFL checker, not structured output +] + +# Fields that are automatically populated from GIMIE/git (not asked to model): +# - name (from GIMIE) +# - gitAuthors (from git extraction) +# - keywords (from GIMIE) +# - dateCreated (from GIMIE, then oldest commit date, then model as fallback) +# - codeRepository (from GIMIE) +# - license (from GIMIE) +# - datePublished (from GIMIE) +# - dateModified (from GIMIE) +# - url (from GIMIE) +# - programmingLanguage (from GIMIE) +# - citation (from GIMIE, if available) +# - readme (from GIMIE) + +# Load model configurations for structured output +# Use a separate config that may disable tools +STRUCTURED_OUTPUT_CONFIGS = load_model_config("run_structured_output") + +# Validate configurations +for config in STRUCTURED_OUTPUT_CONFIGS: + if not validate_config(config): + logger.error(f"Invalid configuration for structured output: {config}") + raise ValueError("Invalid model configuration") + +# Generate simplified model dynamically from SoftwareSourceCode +# Only include fields that should be extracted by the model +# Cache it at module level to avoid regenerating on every call +_SIMPLIFIED_MODEL, _UNION_METADATA = create_simplified_model( + SoftwareSourceCode, + field_filter=MODEL_EXTRACTION_FIELDS, +) + +# System prompt for structured output agent +STRUCTURED_OUTPUT_SYSTEM_PROMPT = """ +You are an expert at extracting structured metadata from repository information. + +Your task is to: +1. Analyze the compiled repository context provided +2. Extract structured metadata according to the simplified schema provided +3. Output only the specified fields with correct data types + +**Important Constraints:** +- Use ONLY primitive types: strings, numbers, lists, and dictionaries +- URLs must be strings (not HttpUrl objects) +- Dates must be ISO format strings (YYYY-MM-DD) +- Enums must be converted to strings +- Do not include fields not in the schema +- All required fields must be present + +**Output Format:** +Return a JSON object matching the provided schema exactly. +""" + + +def get_structured_output_prompt( + compiled_context: CompiledContext, + schema: Dict[str, Any], + example: Optional[Dict[str, Any]] = None, +) -> str: + """ + Generate prompt for structured output agent. + + Args: + compiled_context: Compiled context from first agent + schema: Simplified schema definition + example: Optional example output + + Returns: + Formatted prompt string + """ + prompt = f"""Extract structured metadata from the compiled repository context. + +**Compiled Context:** +{compiled_context.markdown_content} + +**Repository URL:** {compiled_context.repository_url} + +**Expected Output Schema:** +{json.dumps(schema, indent=2)} +""" + + if example: + prompt += f""" + +**Example Output (for reference):** +{json.dumps(example, indent=2)} +""" + + prompt += """ + +Please extract and return structured metadata matching the schema exactly. +Use only primitive types (strings, numbers, lists, dicts). +Convert all URLs, dates, and enums to strings. +""" + + return prompt + + +async def generate_structured_output( + compiled_context: CompiledContext, + schema: Dict[str, Any], + example: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + """ + Generate structured output from compiled context. + + Args: + compiled_context: Compiled context from context compiler + schema: Simplified schema definition + example: Optional example output + + Returns: + Dictionary with 'data' (dynamically generated simplified model), 'usage' (dict with token info), + and 'union_metadata' (dict for Union field reconciliation) + """ + # Create context for the agent + agent_context = { + "compiled_context": compiled_context, + "schema": schema, + } + + # Prepare the prompt + prompt = get_structured_output_prompt(compiled_context, schema, example) + + # No tools for structured output agent + tools = [] + + try: + # Run agent with fallback across multiple models + # Use dynamically generated simplified model + result = await run_agent_with_fallback( + STRUCTURED_OUTPUT_CONFIGS, + prompt, + agent_context, + _SIMPLIFIED_MODEL, + STRUCTURED_OUTPUT_SYSTEM_PROMPT, + tools, # No tools for this agent + ) + + # Extract the output from PydanticAI result + if hasattr(result, "output"): + structured_output = result.output + else: + structured_output = result + + # Estimate tokens from prompt and response + response_text = "" + if hasattr(structured_output, "model_dump_json"): + response_text = structured_output.model_dump_json() + elif isinstance(structured_output, dict): + import json as json_module + + response_text = json_module.dumps(structured_output) + elif isinstance(structured_output, str): + response_text = structured_output + + # Extract usage information from the result + input_tokens = 0 + output_tokens = 0 + tool_calls_count = 0 + + if hasattr(result, "usage"): + usage = result.usage + input_tokens = getattr(usage, "input_tokens", 0) or 0 + output_tokens = getattr(usage, "output_tokens", 0) or 0 + tool_calls_count = getattr(usage, "tool_calls", 0) or 0 + + # Fallback to details field for certain models + if input_tokens == 0 and output_tokens == 0 and hasattr(usage, "details"): + details = usage.details + if isinstance(details, dict): + input_tokens = details.get("input_tokens", 0) or 0 + output_tokens = details.get("output_tokens", 0) or 0 + + # Calculate estimates with tool call support (always, for validation/fallback) + estimated = estimate_tokens_with_tools( + system_prompt=STRUCTURED_OUTPUT_SYSTEM_PROMPT, + user_prompt=prompt, + response=response_text, + tool_calls=tool_calls_count, + tool_results_text=None, + ) + + # Use estimates as primary when API returns 0 + if input_tokens == 0 and output_tokens == 0: + logger.warning( + "API returned 0 tokens, using tiktoken estimates as primary counts", + ) + input_tokens = estimated.get("input_tokens", 0) + output_tokens = estimated.get("output_tokens", 0) + + usage_data = { + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "estimated_input_tokens": estimated.get("input_tokens", 0), + "estimated_output_tokens": estimated.get("output_tokens", 0), + } + + # Log output summary + if hasattr(structured_output, "model_dump"): + output_dict = structured_output.model_dump() + elif isinstance(structured_output, dict): + output_dict = structured_output + else: + output_dict = {} + + logger.info(f"Structured output generated: {len(output_dict)} top-level fields") + + return { + "data": structured_output, + "usage": usage_data, + "union_metadata": _UNION_METADATA, + } + + except Exception as e: + logger.error(f"Structured output generation failed: {e}", exc_info=True) + return { + "data": None, + "usage": None, + "union_metadata": _UNION_METADATA, + } diff --git a/src/agents/atomic_agents/user_classifier.py b/src/agents/atomic_agents/user_classifier.py new file mode 100644 index 0000000..b8edf0f --- /dev/null +++ b/src/agents/atomic_agents/user_classifier.py @@ -0,0 +1,176 @@ +""" +User classifier agent - Classifies user discipline and position. + +This agent takes compiled context and classifies the user's discipline(s) +and position(s) with justifications. +""" + +import logging +from typing import Any, Dict + +from ...llm.model_config import load_model_config, validate_config +from ...utils.token_counter import estimate_tokens_from_messages +from ..agents_management import run_agent_with_fallback +from .models import CompiledContext, UserClassification + +logger = logging.getLogger(__name__) + +# Load model configurations for user classification +USER_CLASSIFIER_CONFIGS = load_model_config("run_user_classifier") + +# Validate configurations +for config in USER_CLASSIFIER_CONFIGS: + if not validate_config(config): + logger.error(f"Invalid configuration for user classifier: {config}") + raise ValueError("Invalid model configuration") + +# System prompt for user classifier +USER_CLASSIFIER_SYSTEM_PROMPT = """ +You are an expert at classifying users by scientific discipline and professional position. + +Your task is to: +1. Analyze the compiled user context provided +2. Determine relevant scientific disciplines (AT LEAST ONE REQUIRED) from the allowed values in the schema +3. Identify professional positions and roles +4. Provide clear justifications for each classification + +**Important Guidelines:** +- discipline: REQUIRED - must select at least one valid discipline from the schema enum +- If multiple disciplines apply, list all relevant ones +- position: List professional positions, roles, or job titles (e.g., "Research Scientist", "PhD Student", "Professor", "Data Engineer") +- Provide evidence-based justifications referencing specific information from the user context +- Use the EXACT discipline names as specified in the JSON schema + +**Note:** Valid discipline values are enforced by the JSON schema enum constraints. +""" + + +def get_user_classifier_prompt(compiled_context: CompiledContext) -> str: + """ + Generate prompt for user classifier agent. + + Args: + compiled_context: Compiled user context from context compiler + + Returns: + Formatted prompt string + """ + prompt = f"""Classify the following user: + +**User Profile URL:** {compiled_context.repository_url} + +**Compiled User Context:** +{compiled_context.markdown_content} + +Please classify: +1. Scientific Disciplines (one or more relevant fields from the allowed list) +2. Professional Positions (roles, job titles, etc.) + +Provide clear justifications for each classification based on the user context. +""" + + logger.debug(f"User classifier prompt length: {len(prompt)} chars") + return prompt + + +async def classify_user_discipline_and_position( + compiled_context: CompiledContext, +) -> Dict[str, Any]: + """ + Classify user discipline and position from compiled context. + + Args: + compiled_context: Compiled user context from context compiler + + Returns: + Dictionary with: + - data: UserClassification object + - usage: Token usage statistics + """ + logger.info("Starting user classification...") + + # Generate prompt + prompt = get_user_classifier_prompt(compiled_context) + + # Prepare agent context (minimal - just pass compiled context) + agent_context = { + "user_url": compiled_context.repository_url, + "compiled_context": compiled_context.markdown_content, + } + + # No tools needed for classification + tools = [] + + logger.debug(f"Prompt length: {len(prompt)} characters") + + # Run agent with schema enforcement + result = await run_agent_with_fallback( + USER_CLASSIFIER_CONFIGS, + prompt, + agent_context, + UserClassification, # Schema enforcement + USER_CLASSIFIER_SYSTEM_PROMPT, + tools, + ) + + # Extract the classification from PydanticAI result + # Check if result has an .output attribute (PydanticAI wrapper) + if hasattr(result, "output"): + classification_data = result.output + else: + classification_data = result + + # Extract usage statistics from result attributes + usage_data = {} + input_tokens = 0 + output_tokens = 0 + + if hasattr(result, "usage"): + usage = result.usage + input_tokens = getattr(usage, "input_tokens", 0) or 0 + output_tokens = getattr(usage, "output_tokens", 0) or 0 + + # Fallback to details field for certain models + if input_tokens == 0 and output_tokens == 0 and hasattr(usage, "details"): + details = usage.details + if isinstance(details, dict): + input_tokens = details.get("input_tokens", 0) + output_tokens = details.get("output_tokens", 0) + + logger.info( + f"User classification usage: {input_tokens} input, {output_tokens} output tokens", + ) + else: + logger.warning("No usage data available from agent") + + usage_data["input_tokens"] = input_tokens + usage_data["output_tokens"] = output_tokens + + # Estimate tokens with tiktoken (serialize model properly) + response_text = "" + if hasattr(classification_data, "model_dump_json"): + response_text = classification_data.model_dump_json() + elif isinstance(classification_data, dict): + import json as json_module + + response_text = json_module.dumps(classification_data) + elif isinstance(classification_data, str): + response_text = classification_data + + estimated = estimate_tokens_from_messages( + system_prompt=USER_CLASSIFIER_SYSTEM_PROMPT, + user_prompt=prompt, + response=response_text, + ) + usage_data["estimated_input_tokens"] = estimated.get("input_tokens", 0) + usage_data["estimated_output_tokens"] = estimated.get("output_tokens", 0) + + logger.info( + f"User classified with {len(classification_data.discipline)} discipline(s) " + f"and {len(classification_data.position)} position(s)", + ) + + return { + "data": classification_data, + "usage": usage_data, + } diff --git a/src/agents/atomic_agents/user_context_compiler.py b/src/agents/atomic_agents/user_context_compiler.py new file mode 100644 index 0000000..810c54a --- /dev/null +++ b/src/agents/atomic_agents/user_context_compiler.py @@ -0,0 +1,432 @@ +""" +User context compiler agent - First stage of atomic agent pipeline. + +This agent uses tools to gather comprehensive user information +and compile it into a markdown document for the next agent. +""" + +import json +import logging +from typing import Any, Dict, Optional + +import httpx + +from ...context.infoscience import ( + get_author_publications_tool, + search_infoscience_authors_tool, + search_infoscience_labs_tool, +) +from ...llm.model_config import load_model_config, validate_config +from ...utils.token_counter import ( + estimate_tokens_with_tools, +) +from ..agents_management import run_agent_with_fallback +from .models import CompiledContext + +logger = logging.getLogger(__name__) + +# Load model configurations for user context compilation +USER_CONTEXT_COMPILER_CONFIGS = load_model_config("run_user_context_compiler") + +# Validate configurations +for config in USER_CONTEXT_COMPILER_CONFIGS: + if not validate_config(config): + logger.error(f"Invalid configuration for user context compiler: {config}") + raise ValueError("Invalid model configuration") + +# System prompt for user context compiler +USER_CONTEXT_COMPILER_SYSTEM_PROMPT = """ +You are an expert at gathering and compiling comprehensive information about GitHub users and researchers. + +Your task is to: +1. Analyze the GitHub user metadata provided (bio, README, ORCID, organizations, etc.) +2. Use available tools to search for additional information: + - Search ORCID for author information and affiliations + - Search Infoscience for EPFL authors and researchers (persona) + - Search Infoscience for EPFL labs and organizational units (orgunit) + - Search the web for additional context about the user + - Get publications by the user from Infoscience +3. Compile all information into a well-structured markdown document + +**Input Sources:** +- GitHub user metadata: Bio, README, ORCID, organizations, location, company, etc. +- Tool results: ORCID records, Infoscience author/lab searches, web search results, publications + +**Output Format:** +Return ONLY a comprehensive markdown document (plain text, not JSON) that includes: +- User overview and professional background +- Affiliations and organizations (from GitHub, ORCID, Infoscience) +- Research interests and disciplines (inferred from bio, publications, affiliations) +- Professional positions and roles +- Publications and research outputs (if found) +- Any other relevant information from GitHub metadata and tool searches + +The compiled context should be thorough and well-organized for the next agent to extract structured metadata. + +**IMPORTANT:** Return ONLY the markdown document as plain text. Do not wrap it in JSON, do not add any explanatory text, do not use code blocks. Just return the raw markdown text. +""" + + +async def search_orcid_tool( + author_name: str, + email: Optional[str] = None, +) -> str: + """ + Search the ORCID API for author information. + + Args: + author_name: The author's name to search for + email: Optional email address to help narrow the search + + Returns: + JSON string with ORCID search results including ORCID IDs, names, and affiliations + """ + logger.info(f"🔍 Agent tool called: search_orcid_tool('{author_name}', '{email}')") + + try: + # Build search query + query_parts = [] + + # Add name to query + if author_name: + # Try to parse first and last name + name_parts = author_name.strip().split() + if len(name_parts) >= 2: + given_name = name_parts[0] + family_name = " ".join(name_parts[1:]) + query_parts.append(f"given-names:{given_name}") + query_parts.append(f"family-name:{family_name}") + else: + query_parts.append(f"family-name:{author_name}") + + # Add email to query if provided + if email: + query_parts.append(f"email:{email}") + + if not query_parts: + return json.dumps({"error": "No search criteria provided"}) + + query = " AND ".join(query_parts) + + async with httpx.AsyncClient() as client: + headers = { + "Accept": "application/json", + } + response = await client.get( + "https://pub.orcid.org/v3.0/search/", + params={"q": query}, + headers=headers, + timeout=10.0, + ) + response.raise_for_status() + data = response.json() + + # Extract relevant information from results + results = [] + num_found = data.get("num-found", 0) + + if num_found == 0: + logger.info(f"⚠ No ORCID records found for '{author_name}'") + return json.dumps({"query": query, "results": [], "num_found": 0}) + + for result in data.get("result", [])[:5]: # Top 5 results + orcid_id = result.get("orcid-identifier", {}).get("path") + + # Get basic info from search result + person_info = { + "orcid_id": f"https://orcid.org/{orcid_id}" if orcid_id else None, + "given_names": result.get("given-names"), + "family_name": result.get("family-name"), + "credit_name": result.get("credit-name"), + } + + # Note: Full affiliation details require a separate API call + if orcid_id: + person_info[ + "note" + ] = f"Full affiliation details available at https://pub.orcid.org/v3.0/{orcid_id}/employments" + + results.append(person_info) + + logger.info( + f"✓ ORCID search for '{author_name}' returned {len(results)} results", + ) + return json.dumps( + { + "query": query, + "results": results, + "num_found": num_found, + }, + indent=2, + ) + + except Exception as e: + logger.error(f"✗ Error searching ORCID for '{author_name}': {e}") + return json.dumps({"error": str(e)}) + + +async def search_web_tool( + query: str, +) -> str: + """ + Search DuckDuckGo for information about a person. + + Args: + query: The search query about a person (e.g., "John Smith EPFL researcher") + + Returns: + Summary of search results from DuckDuckGo (JSON string) + """ + logger.info(f"🔍 Agent tool called: search_web_tool('{query}')") + + try: + # Simple DuckDuckGo search using their instant answer API + async with httpx.AsyncClient() as client: + response = await client.get( + "https://api.duckduckgo.com/", + params={ + "q": query, + "format": "json", + "no_html": "1", + "skip_disambig": "1", + }, + timeout=10.0, + ) + response.raise_for_status() + data = response.json() + + results = [] + # Extract abstract if available + if data.get("Abstract"): + results.append( + { + "title": data.get("Heading", ""), + "abstract": data.get("Abstract", ""), + "url": data.get("AbstractURL", ""), + }, + ) + + # Extract related topics + for topic in data.get("RelatedTopics", [])[:5]: + if isinstance(topic, dict) and "Text" in topic: + results.append( + { + "title": topic.get("Text", ""), + "url": topic.get("FirstURL", ""), + }, + ) + + logger.info( + f"✓ Web search for '{query}' returned {len(results)} results", + ) + return json.dumps( + { + "query": query, + "results": results, + }, + indent=2, + ) + + except Exception as e: + logger.error(f"✗ Error searching web for '{query}': {e}") + return json.dumps({"error": str(e)}) + + +def get_user_context_compiler_prompt( + username: str, + user_url: str, + github_metadata: Dict[str, Any], +) -> str: + """ + Generate prompt for user context compiler agent. + + Args: + username: GitHub username + user_url: GitHub user profile URL + github_metadata: GitHub user metadata dict + + Returns: + Formatted prompt string + """ + prompt = f"""Compile comprehensive information about this GitHub user: + +**GitHub Username:** {username} +**GitHub Profile URL:** {user_url} + +**GitHub User Metadata:** +{json.dumps(github_metadata, indent=2, default=str)} +""" + + prompt += """ + +Please: +1. Analyze the GitHub user metadata provided +2. Use available tools to search for additional information: + - Search ORCID if the user has an ORCID ID or if you need to find their ORCID profile + - Search Infoscience for EPFL authors/researchers (persona search) using the user's name + - Search Infoscience for EPFL labs/organizational units (orgunit search) if organizations are mentioned + - Search the web for additional context about the user + - Get publications by the user from Infoscience if found +3. Compile all information into a comprehensive markdown document + +Focus on gathering information that will help extract: +- User's professional background and affiliations +- Research interests and scientific disciplines +- Professional positions and roles +- Organizations the user is affiliated with +- Publications and research outputs + +**IMPORTANT:** Return ONLY the markdown document as plain text. Do not wrap it in JSON, do not add any explanatory text, do not use code blocks. Just return the raw markdown text. +""" + + logger.debug(f"User context compiler prompt length: {len(prompt)} chars") + return prompt + + +async def compile_user_context( + username: str, + user_url: str, + github_metadata: Dict[str, Any], +) -> Dict[str, Any]: + """ + Compile user context using tools to gather comprehensive information. + + Args: + username: GitHub username + user_url: GitHub user profile URL + github_metadata: GitHub user metadata dict + + Returns: + Dictionary with 'data' (CompiledContext) and 'usage' (dict with token info) + """ + # Create context for the agent + agent_context = { + "username": username, + "user_url": user_url, + "github_metadata": github_metadata, + } + + # Prepare the prompt + prompt = get_user_context_compiler_prompt(username, user_url, github_metadata) + + # Define tools for the user context compiler + tools = [ + search_orcid_tool, + search_infoscience_authors_tool, + search_infoscience_labs_tool, + get_author_publications_tool, + search_web_tool, + ] + + try: + # Run agent with fallback across multiple models + # Use str as output type - agent returns markdown text + result = await run_agent_with_fallback( + USER_CONTEXT_COMPILER_CONFIGS, + prompt, + agent_context, + str, # Simple string output - just markdown text + USER_CONTEXT_COMPILER_SYSTEM_PROMPT, + tools, + ) + + # Extract the markdown string from PydanticAI result + if hasattr(result, "output"): + markdown_content = result.output + else: + markdown_content = result + + # Convert string to CompiledContext + if isinstance(markdown_content, str): + compiled_context = CompiledContext( + markdown_content=markdown_content, + repository_url=user_url, # Use user_url as repository_url for consistency + summary=None, + ) + else: + # Fallback if we get something unexpected + compiled_context = CompiledContext( + markdown_content=str(markdown_content), + repository_url=user_url, + summary=None, + ) + + # Estimate tokens from prompt and response + response_text = "" + if hasattr(compiled_context, "model_dump_json"): + response_text = compiled_context.model_dump_json() + elif isinstance(compiled_context, dict): + import json as json_module + + response_text = json_module.dumps(compiled_context) + elif isinstance(compiled_context, str): + response_text = compiled_context + + # Extract usage information from the result + input_tokens = 0 + output_tokens = 0 + tool_calls_count = 0 + + if hasattr(result, "usage"): + usage = result.usage + input_tokens = getattr(usage, "input_tokens", 0) or 0 + output_tokens = getattr(usage, "output_tokens", 0) or 0 + tool_calls_count = getattr(usage, "tool_calls", 0) or 0 + + # Fallback to details field for certain models + if input_tokens == 0 and output_tokens == 0 and hasattr(usage, "details"): + details = usage.details + if isinstance(details, dict): + input_tokens = details.get("input_tokens", 0) or 0 + output_tokens = details.get("output_tokens", 0) or 0 + + # Calculate estimates with tool call support (always, for validation/fallback) + estimated = estimate_tokens_with_tools( + system_prompt=USER_CONTEXT_COMPILER_SYSTEM_PROMPT, + user_prompt=prompt, + response=response_text, + tool_calls=tool_calls_count, + tool_results_text=None, + ) + + # Use estimates as primary when API returns 0 + if input_tokens == 0 and output_tokens == 0: + logger.warning( + "API returned 0 tokens, using tiktoken estimates as primary counts", + ) + input_tokens = estimated.get("input_tokens", 0) + output_tokens = estimated.get("output_tokens", 0) + + usage_data = { + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "estimated_input_tokens": estimated.get("input_tokens", 0), + "estimated_output_tokens": estimated.get("output_tokens", 0), + } + + # Log compiled context size + if hasattr(compiled_context, "markdown_content"): + content_size = len(compiled_context.markdown_content) + elif ( + isinstance(compiled_context, dict) + and "markdown_content" in compiled_context + ): + content_size = len(compiled_context.get("markdown_content", "")) + else: + content_size = 0 + + logger.info( + f"User context compilation completed: {content_size:,} chars of markdown", + ) + + return { + "data": compiled_context, + "usage": usage_data, + } + + except Exception as e: + logger.error(f"User context compilation failed: {e}", exc_info=True) + return { + "data": None, + "usage": None, + } diff --git a/src/agents/atomic_agents/user_structured_output.py b/src/agents/atomic_agents/user_structured_output.py new file mode 100644 index 0000000..f830ff6 --- /dev/null +++ b/src/agents/atomic_agents/user_structured_output.py @@ -0,0 +1,235 @@ +""" +User structured output agent - Second stage of atomic agent pipeline. + +This agent takes compiled context and simplified schema instructions +to produce structured metadata output for users. +""" + +import json +import logging +from typing import Any, Dict, Optional + +from ...data_models.conversion import create_simplified_model +from ...data_models.user import GitHubUser +from ...llm.model_config import load_model_config, validate_config +from ...utils.token_counter import ( + estimate_tokens_with_tools, +) +from ..agents_management import run_agent_with_fallback +from .models import CompiledContext + +logger = logging.getLogger(__name__) + +# Configuration: Fields that should be extracted by the LLM model +# Only basic identity fields - enrichment fields handled by specialized agents +MODEL_EXTRACTION_FIELDS = [ + "name", + "fullname", + "githubHandle", +] + +# Load model configurations for user structured output +USER_STRUCTURED_OUTPUT_CONFIGS = load_model_config("run_user_structured_output") + +# Validate configurations +for config in USER_STRUCTURED_OUTPUT_CONFIGS: + if not validate_config(config): + logger.error(f"Invalid configuration for user structured output: {config}") + raise ValueError("Invalid model configuration") + +# Generate simplified model dynamically from GitHubUser +# Only include fields that should be extracted by the model +# Cache it at module level to avoid regenerating on every call +_SIMPLIFIED_MODEL, _UNION_METADATA = create_simplified_model( + GitHubUser, + field_filter=MODEL_EXTRACTION_FIELDS, +) + +# System prompt for user structured output agent +USER_STRUCTURED_OUTPUT_SYSTEM_PROMPT = """ +You are an expert at extracting structured metadata from user information. + +Your task is to: +1. Analyze the compiled user context provided +2. Extract basic identity fields according to the simplified schema provided +3. Output only the specified fields with correct data types + +**Important Constraints:** +- Use ONLY primitive types: strings, numbers, lists, and dictionaries +- URLs must be strings (not HttpUrl objects) +- Do not include fields not in the schema +- All required fields must be present + +**Output Format:** +Return a JSON object matching the provided schema exactly. +""" + + +def get_user_structured_output_prompt( + compiled_context: CompiledContext, + schema: Dict[str, Any], + example: Optional[Dict[str, Any]] = None, +) -> str: + """ + Generate prompt for user structured output agent. + + Args: + compiled_context: Compiled context from first agent + schema: Simplified schema definition + example: Optional example output + + Returns: + Formatted prompt string + """ + prompt = f"""Extract basic identity metadata from the compiled user context. + +**Compiled Context:** +{compiled_context.markdown_content} + +**User Profile URL:** {compiled_context.repository_url} + +**Expected Output Schema:** +{json.dumps(schema, indent=2)} +""" + + if example: + prompt += f""" + +**Example Output (for reference):** +{json.dumps(example, indent=2)} +""" + + prompt += """ + +Please extract and return basic identity fields matching the schema exactly. +Use only primitive types (strings, numbers, lists, dicts). +""" + + return prompt + + +async def generate_user_structured_output( + compiled_context: CompiledContext, + schema: Dict[str, Any], + example: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + """ + Generate structured output from compiled user context. + + Args: + compiled_context: Compiled context from user context compiler + schema: Simplified schema definition + example: Optional example output + + Returns: + Dictionary with 'data' (dynamically generated simplified model), 'usage' (dict with token info), + and 'union_metadata' (dict for Union field reconciliation) + """ + # Create context for the agent + agent_context = { + "compiled_context": compiled_context, + "schema": schema, + } + + # Prepare the prompt + prompt = get_user_structured_output_prompt(compiled_context, schema, example) + + # No tools for structured output agent + tools = [] + + try: + # Run agent with fallback across multiple models + # Use dynamically generated simplified model + result = await run_agent_with_fallback( + USER_STRUCTURED_OUTPUT_CONFIGS, + prompt, + agent_context, + _SIMPLIFIED_MODEL, + USER_STRUCTURED_OUTPUT_SYSTEM_PROMPT, + tools, # No tools for this agent + ) + + # Extract the output from PydanticAI result + if hasattr(result, "output"): + structured_output = result.output + else: + structured_output = result + + # Estimate tokens from prompt and response + response_text = "" + if hasattr(structured_output, "model_dump_json"): + response_text = structured_output.model_dump_json() + elif isinstance(structured_output, dict): + import json as json_module + + response_text = json_module.dumps(structured_output) + elif isinstance(structured_output, str): + response_text = structured_output + + # Extract usage information from the result + input_tokens = 0 + output_tokens = 0 + tool_calls_count = 0 + + if hasattr(result, "usage"): + usage = result.usage + input_tokens = getattr(usage, "input_tokens", 0) or 0 + output_tokens = getattr(usage, "output_tokens", 0) or 0 + tool_calls_count = getattr(usage, "tool_calls", 0) or 0 + + # Fallback to details field for certain models + if input_tokens == 0 and output_tokens == 0 and hasattr(usage, "details"): + details = usage.details + if isinstance(details, dict): + input_tokens = details.get("input_tokens", 0) or 0 + output_tokens = details.get("output_tokens", 0) or 0 + + # Calculate estimates with tool call support (always, for validation/fallback) + estimated = estimate_tokens_with_tools( + system_prompt=USER_STRUCTURED_OUTPUT_SYSTEM_PROMPT, + user_prompt=prompt, + response=response_text, + tool_calls=tool_calls_count, + tool_results_text=None, + ) + + # Use estimates as primary when API returns 0 + if input_tokens == 0 and output_tokens == 0: + logger.warning( + "API returned 0 tokens, using tiktoken estimates as primary counts", + ) + input_tokens = estimated.get("input_tokens", 0) + output_tokens = estimated.get("output_tokens", 0) + + usage_data = { + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "estimated_input_tokens": estimated.get("input_tokens", 0), + "estimated_output_tokens": estimated.get("output_tokens", 0), + } + + # Log output summary + if hasattr(structured_output, "model_dump"): + output_dict = structured_output.model_dump() + elif isinstance(structured_output, dict): + output_dict = structured_output + else: + output_dict = {} + + logger.info( + f"User structured output generated: {len(output_dict)} top-level fields", + ) + + return { + "data": structured_output, + "usage": usage_data, + "union_metadata": _UNION_METADATA, + } + + except Exception as e: + logger.error(f"User structured output generation failed: {e}", exc_info=True) + return { + "data": None, + "usage": None, + "union_metadata": _UNION_METADATA, + } diff --git a/src/agents/epfl_assessment.py b/src/agents/epfl_assessment.py index f73740a..c693c83 100644 --- a/src/agents/epfl_assessment.py +++ b/src/agents/epfl_assessment.py @@ -12,7 +12,9 @@ load_model_config, validate_config, ) -from ..utils.token_counter import estimate_tokens_from_messages +from ..utils.token_counter import ( + estimate_tokens_with_tools, +) from .agents_management import run_agent_with_fallback from .epfl_assessment_prompts import ( epfl_assessment_system_prompt, @@ -102,26 +104,40 @@ async def assess_epfl_relationship( if hasattr(assessment_data, "model_dump_json") else "" ) - estimated = estimate_tokens_from_messages( - system_prompt=epfl_assessment_system_prompt, - user_prompt=prompt, - response=response_text, - ) - # Extract actual token usage from result input_tokens = 0 output_tokens = 0 + tool_calls_count = 0 + if hasattr(result, "usage"): usage = result.usage input_tokens = getattr(usage, "input_tokens", 0) or 0 output_tokens = getattr(usage, "output_tokens", 0) or 0 + tool_calls_count = getattr(usage, "tool_calls", 0) or 0 # Fallback to details if needed if input_tokens == 0 and output_tokens == 0 and hasattr(usage, "details"): details = usage.details if isinstance(details, dict): - input_tokens = details.get("input_tokens", 0) - output_tokens = details.get("output_tokens", 0) + input_tokens = details.get("input_tokens", 0) or 0 + output_tokens = details.get("output_tokens", 0) or 0 + + # Calculate estimates with tool call support (always, for validation/fallback) + estimated = estimate_tokens_with_tools( + system_prompt=epfl_assessment_system_prompt, + user_prompt=prompt, + response=response_text, + tool_calls=tool_calls_count, + tool_results_text=None, + ) + + # Use estimates as primary when API returns 0 + if input_tokens == 0 and output_tokens == 0: + logger.warning( + "API returned 0 tokens, using tiktoken estimates as primary counts", + ) + input_tokens = estimated.get("input_tokens", 0) + output_tokens = estimated.get("output_tokens", 0) # Return with usage statistics return { diff --git a/src/agents/linked_entities_enrichment.py b/src/agents/linked_entities_enrichment.py new file mode 100644 index 0000000..136c89a --- /dev/null +++ b/src/agents/linked_entities_enrichment.py @@ -0,0 +1,932 @@ +""" +linked entities Enrichment Agent + +This agent searches academic catalogs (Infoscience, OpenAlex, EPFL Graph) to find +related publications, persons, and organizational units. +""" + +import logging +from typing import Any + +from pydantic import HttpUrl, ValidationError +from pydantic_ai import Agent + +from ..context.infoscience import ( + clear_infoscience_cache, + get_author_publications_tool, + search_infoscience_authors_tool, + search_infoscience_labs_tool, + search_infoscience_publications_tool, +) +from ..data_models.conversion import create_simplified_model +from ..data_models.linked_entities import ( + CatalogType, + EntityType, + linkedEntitiesEnrichmentResult, + linkedEntitiesRelation, +) +from ..llm.model_config import ( + create_pydantic_ai_model, + load_model_config, + validate_config, +) +from ..utils.token_counter import estimate_tokens_from_messages +from .linked_entities_prompts import ( + get_organization_linked_entities_prompt, + get_repository_linked_entities_prompt, + get_user_linked_entities_prompt, + linked_entities_system_prompt, +) +from .url_validation import validate_infoscience_url + +logger = logging.getLogger(__name__) + +# Load model configuration for linked entities enrichment +linked_entities_configs = load_model_config("run_linked_entities_enrichment") + +# Validate configurations +for config in linked_entities_configs: + if not validate_config(config): + logger.error( + f"Invalid configuration for linked entities enrichment: {config}", + ) + raise ValueError("Invalid model configuration") + +# Create simplified model for linked entities enrichment +# This converts HttpUrl fields to str with format instructions +( + _SIMPLIFIED_LINKED_ENTITIES_MODEL, + _LINKED_ENTITIES_UNION_METADATA, +) = create_simplified_model(linkedEntitiesEnrichmentResult) + +# Track active agents for cleanup +_active_linked_entities_agents = [] + + +def _convert_simplified_to_full_linked_entities( + simplified_output: Any, +) -> linkedEntitiesEnrichmentResult: + """ + Convert simplified linked entities output (with str URLs) to full model (with HttpUrl). + + Args: + simplified_output: Simplified model output with string URLs + + Returns: + Full linkedEntitiesEnrichmentResult with HttpUrl fields validated + """ + logger.debug(f"Simplified output from LLM: {simplified_output}") + # Convert to dict if it's a Pydantic model + if hasattr(simplified_output, "model_dump"): + data = simplified_output.model_dump() + elif isinstance(simplified_output, dict): + data = simplified_output + else: + data = simplified_output + + # Recursively convert URL strings to HttpUrl in nested entities + def convert_urls_in_entity(entity_dict: dict, entity_type: str = None) -> dict: + """Convert URL strings to HttpUrl in entity dictionaries.""" + if not isinstance(entity_dict, dict): + return entity_dict + + converted = entity_dict.copy() + + # Clean up None values in list fields (convert to empty list) + for list_field in ["subjects", "authors", "keywords", "research_areas"]: + if list_field in converted and converted[list_field] is None: + converted[list_field] = [] + + # Construct URL from UUID if URL is missing + uuid = converted.get("uuid") + if uuid and not converted.get("url") and not converted.get("profile_url"): + # Construct URL based on entity type + if ( + entity_type == "publication" + or "publication" in str(converted.get("type", "")).lower() + ): + converted[ + "url" + ] = f"https://infoscience.epfl.ch/entities/publication/{uuid}" + elif ( + entity_type == "person" + or "author" in str(converted.get("type", "")).lower() + ): + converted[ + "profile_url" + ] = f"https://infoscience.epfl.ch/entities/person/{uuid}" + elif ( + entity_type == "orgunit" + or "orgunit" in str(converted.get("type", "")).lower() + or "lab" in str(converted.get("type", "")).lower() + ): + converted[ + "url" + ] = f"https://infoscience.epfl.ch/entities/orgunit/{uuid}" + + # Convert url fields to HttpUrl + for url_field in ["url", "profile_url", "repository_url"]: + if url_field in converted and converted[url_field]: + try: + # Validate and convert string to HttpUrl + converted[url_field] = HttpUrl(converted[url_field]) + except (ValueError, ValidationError) as e: + logger.warning( + f"Invalid URL format for {url_field}: {converted[url_field]}, error: {e}", + ) + # Keep as string if validation fails (will be handled by model validation) + + # Recursively convert nested entities + if "entityInfosciencePublication" in converted: + converted["entityInfosciencePublication"] = convert_urls_in_entity( + converted["entityInfosciencePublication"], + entity_type="publication", + ) + if "entityInfoscienceAuthor" in converted: + converted["entityInfoscienceAuthor"] = convert_urls_in_entity( + converted["entityInfoscienceAuthor"], + entity_type="person", + ) + if "entityInfoscienceOrgUnit" in converted: + converted["entityInfoscienceOrgUnit"] = convert_urls_in_entity( + converted["entityInfoscienceOrgUnit"], + entity_type="orgunit", + ) + + return converted + + # Convert relations recursively + def convert_relations(relations: list) -> list: + """Convert URL strings in relations and reconcile Union fields.""" + converted_relations = [] + for rel in relations: + if isinstance(rel, dict): + # Then convert URLs to HttpUrl + converted_rel = convert_urls_in_entity( + rel, + entity_type=rel.get("entityType"), + ) + converted_relations.append(converted_rel) + else: + converted_relations.append(rel) + return converted_relations + + # Convert repository_relations + if "repository_relations" in data and data["repository_relations"]: + data["repository_relations"] = convert_relations(data["repository_relations"]) + + # Convert author_relations (dict of lists) + if "author_relations" in data and data["author_relations"]: + converted_author_relations = {} + for author_name, relations in data["author_relations"].items(): + converted_author_relations[author_name] = convert_relations(relations) + data["author_relations"] = converted_author_relations + + # Convert organization_relations (dict of lists) + if "organization_relations" in data and data["organization_relations"]: + converted_org_relations = {} + for org_name, relations in data["organization_relations"].items(): + converted_org_relations[org_name] = convert_relations(relations) + data["organization_relations"] = converted_org_relations + + # Create full model from converted data + # First, validate and create linkedEntitiesRelation objects from the relations + try: + # Convert repository_relations + converted_repo_relations = [] + if "repository_relations" in data and data["repository_relations"]: + for rel_dict in data["repository_relations"]: + # Apply None-to-empty-list conversion to nested entities + if rel_dict.get("entityInfosciencePublication"): + rel_dict["entityInfosciencePublication"] = convert_urls_in_entity( + rel_dict["entityInfosciencePublication"], + entity_type="publication", + ) + if rel_dict.get("entityInfoscienceAuthor"): + rel_dict["entityInfoscienceAuthor"] = convert_urls_in_entity( + rel_dict["entityInfoscienceAuthor"], + entity_type="person", + ) + if rel_dict.get("entityInfoscienceOrgUnit"): + rel_dict["entityInfoscienceOrgUnit"] = convert_urls_in_entity( + rel_dict["entityInfoscienceOrgUnit"], + entity_type="orgunit", + ) + try: + converted_repo_relations.append(linkedEntitiesRelation(**rel_dict)) + except ValidationError as e: + logger.warning( + f"Failed to create repository relation: {e}, skipping relation", + ) + logger.debug(f"Failed relation dict: {rel_dict}") + + # Convert author_relations (dict of lists) + converted_author_relations = {} + if "author_relations" in data and data["author_relations"]: + for author_name, relations in data["author_relations"].items(): + converted_author_relations[author_name] = [] + for rel_dict in relations: + # Apply None-to-empty-list conversion to nested entities + if rel_dict.get("entityInfosciencePublication"): + rel_dict[ + "entityInfosciencePublication" + ] = convert_urls_in_entity( + rel_dict["entityInfosciencePublication"], + entity_type="publication", + ) + if rel_dict.get("entityInfoscienceAuthor"): + rel_dict["entityInfoscienceAuthor"] = convert_urls_in_entity( + rel_dict["entityInfoscienceAuthor"], + entity_type="person", + ) + if rel_dict.get("entityInfoscienceOrgUnit"): + rel_dict["entityInfoscienceOrgUnit"] = convert_urls_in_entity( + rel_dict["entityInfoscienceOrgUnit"], + entity_type="orgunit", + ) + try: + converted_author_relations[author_name].append( + linkedEntitiesRelation(**rel_dict), + ) + except ValidationError as e: + logger.warning( + f"Failed to create author relation for {author_name}: {e}, skipping relation", + ) + logger.debug(f"Failed relation dict: {rel_dict}") + + # Convert organization_relations (dict of lists) + converted_org_relations = {} + if "organization_relations" in data and data["organization_relations"]: + for org_name, relations in data["organization_relations"].items(): + converted_org_relations[org_name] = [] + for rel_dict in relations: + # Apply None-to-empty-list conversion to nested entities + if rel_dict.get("entityInfosciencePublication"): + rel_dict[ + "entityInfosciencePublication" + ] = convert_urls_in_entity( + rel_dict["entityInfosciencePublication"], + entity_type="publication", + ) + if rel_dict.get("entityInfoscienceAuthor"): + rel_dict["entityInfoscienceAuthor"] = convert_urls_in_entity( + rel_dict["entityInfoscienceAuthor"], + entity_type="person", + ) + if rel_dict.get("entityInfoscienceOrgUnit"): + rel_dict["entityInfoscienceOrgUnit"] = convert_urls_in_entity( + rel_dict["entityInfoscienceOrgUnit"], + entity_type="orgunit", + ) + try: + converted_org_relations[org_name].append( + linkedEntitiesRelation(**rel_dict), + ) + except ValidationError as e: + logger.warning( + f"Failed to create organization relation for {org_name}: {e}, skipping relation", + ) + logger.debug(f"Failed relation dict: {rel_dict}") + + # Create the full result with converted relations + result = linkedEntitiesEnrichmentResult( + repository_relations=converted_repo_relations, + author_relations=converted_author_relations, + organization_relations=converted_org_relations, + searchStrategy=data.get("searchStrategy"), + catalogsSearched=data.get("catalogsSearched", []), + totalSearches=data.get("totalSearches", 0), + inputTokens=data.get("inputTokens"), + outputTokens=data.get("outputTokens"), + ) + + # Populate the `entity` field for convenience + for relation in result.relations: + if relation.entityType == EntityType.PUBLICATION: + relation.entity = relation.entityInfosciencePublication + elif relation.entityType == EntityType.PERSON: + relation.entity = relation.entityInfoscienceAuthor + elif relation.entityType == EntityType.ORGUNIT: + relation.entity = relation.entityInfoscienceOrgUnit + + return result + except ValidationError as e: + logger.error( + f"Failed to create linkedEntitiesEnrichmentResult: {e}", + exc_info=True, + ) + logger.debug(f"Failed data: {data}") + # Return a minimal valid result if conversion fails + return linkedEntitiesEnrichmentResult( + repository_relations=[], + author_relations={}, + organization_relations={}, + ) + + +def create_linked_entities_agent(config: dict) -> Agent: + """Create an linked entities enrichment agent from configuration.""" + model = create_pydantic_ai_model(config) + + # Define tools for the agent + tools = [ + search_infoscience_publications_tool, + search_infoscience_authors_tool, + search_infoscience_labs_tool, + get_author_publications_tool, + ] + + # Use simplified model that converts HttpUrl to str + agent = Agent( + model=model, + output_type=_SIMPLIFIED_LINKED_ENTITIES_MODEL, + system_prompt=linked_entities_system_prompt, + tools=tools, + retries=3, # Allow model to retry up to 3 times on validation errors + ) + + # Track agent for cleanup + _active_linked_entities_agents.append(agent) + + return agent + + +async def cleanup_linked_entities_agents(): + """Cleanup linked entities enrichment agents to free memory.""" + global _active_linked_entities_agents + for agent in _active_linked_entities_agents: + try: + # Close/cleanup if the agent has such methods + if hasattr(agent, "close"): + await agent.close() + except Exception as e: + logger.warning(f"Error cleaning up catalog agent: {e}") + _active_linked_entities_agents = [] + + +async def run_agent_with_fallback( + agent_configs: list[dict], + prompt: str, +) -> Any: + """ + Run the linked entities enrichment agent with fallback across multiple models. + + Args: + agent_configs: List of model configurations to try + prompt: The enrichment prompt + + Returns: + Result dict with 'data' and 'usage' keys + """ + last_exception = None + + for idx, config in enumerate(agent_configs): + try: + logger.info( + f"Attempting linked entities enrichment with model {idx + 1}/{len(agent_configs)}: {config.get('model')}", + ) + + # Create agent + agent = create_linked_entities_agent(config) + + # Run the agent + logger.info(f"Prompt length: {len(prompt)} chars") + result = await agent.run(prompt) + + # Extract output and usage + output = result.output + + # Extract token usage + usage_data = {} + if hasattr(result, "usage"): + usage = result.usage + input_tokens = getattr(usage, "input_tokens", 0) or 0 + output_tokens = getattr(usage, "output_tokens", 0) or 0 + + # Fallback to details field for certain models + if ( + input_tokens == 0 + and output_tokens == 0 + and hasattr(usage, "details") + ): + details = usage.details + if isinstance(details, dict): + input_tokens = details.get("input_tokens", 0) + output_tokens = details.get("output_tokens", 0) + + usage_data = { + "input_tokens": input_tokens, + "output_tokens": output_tokens, + } + + logger.info( + f"✓ linked entities enrichment succeeded with {input_tokens} input, {output_tokens} output tokens", + ) + + # Estimate tokens as fallback + response_text = ( + output.model_dump_json() if hasattr(output, "model_dump_json") else "" + ) + estimated = estimate_tokens_from_messages( + system_prompt=linked_entities_system_prompt, + user_prompt=prompt, + response=response_text, + ) + + usage_data["estimated_input_tokens"] = estimated.get("input_tokens", 0) + usage_data["estimated_output_tokens"] = estimated.get("output_tokens", 0) + + # Convert simplified output (with str URLs) back to full model (with HttpUrl) + # The simplified model returns strings for URL fields, we need to convert them + full_output = _convert_simplified_to_full_linked_entities(output) + + # Debug logging: Log what we got from the LLM + if hasattr(full_output, "organization_relations"): + logger.debug( + f"Organization relations after conversion: {len(full_output.organization_relations)} organizations", + ) + for org_name, relations in full_output.organization_relations.items(): + logger.debug( + f" Organization '{org_name}': {len(relations)} relations", + ) + for idx, rel in enumerate(relations): + entity = None + if rel.entityType == EntityType.PUBLICATION: + entity = rel.entityInfosciencePublication + elif rel.entityType == EntityType.PERSON: + entity = rel.entityInfoscienceAuthor + elif rel.entityType == EntityType.ORGUNIT: + entity = rel.entityInfoscienceOrgUnit + + logger.debug( + f" Relation {idx}: entityType={rel.entityType}, " + f"has_entity={entity is not None}", + ) + if entity: + if hasattr(entity, "uuid"): + logger.debug(f" Entity UUID: {entity.uuid}") + if hasattr(entity, "url"): + logger.debug(f" Entity URL: {entity.url}") + if hasattr(entity, "profile_url"): + logger.debug( + f" Entity profile_url: {entity.profile_url}", + ) + + return {"data": full_output, "usage": usage_data} + + except Exception as e: + error_msg = str(e) + logger.warning( + f"linked entities enrichment failed with model {config.get('model')}: {e}", + ) + + # Log detailed validation errors + if "validation" in error_msg.lower() or "retries" in error_msg.lower(): + logger.error( + f"Agent run failed with validation error: {e}", + exc_info=True, + ) + + # Traverse nested exception chains + if hasattr(e, "__cause__") and e.__cause__: + logger.error(f"Underlying cause: {e.__cause__}") + cause = e.__cause__ + depth = 0 + while hasattr(cause, "__cause__") and cause.__cause__ and depth < 5: + cause = cause.__cause__ + logger.error(f"Nested cause (depth {depth + 1}): {cause}") + depth += 1 + + last_exception = e + continue + + logger.error( + f"All linked entities enrichment models failed. Last error: {last_exception}", + ) + raise (last_exception or Exception("All linked entities enrichment models failed")) + + +async def _validate_infoscience_relations( + relations: list[linkedEntitiesRelation], +) -> list[linkedEntitiesRelation]: + """ + Validate and normalize Infoscience URLs in linked entities relations. + + Args: + relations: List of linkedEntitiesRelation objects + + Returns: + Filtered list with validated relations (invalid ones removed) + """ + validated_relations = [] + + for relation in relations: + # Only validate Infoscience relations + if relation.catalogType != CatalogType.INFOSCIENCE: + validated_relations.append(relation) + continue + + entity = None + if relation.entityType == EntityType.PUBLICATION: + entity = relation.entityInfosciencePublication + elif relation.entityType == EntityType.PERSON: + entity = relation.entityInfoscienceAuthor + elif relation.entityType == EntityType.ORGUNIT: + entity = relation.entityInfoscienceOrgUnit + + # Debug: Log relation details before validation + logger.debug( + f"Validating relation: entityType={relation.entityType}, " + f"has_entity={entity is not None}, " + f"entity_type={type(entity).__name__ if entity else 'None'}", + ) + + # Get entity URL using the get_url() method which handles all cases + entity_url = None + if entity: + if hasattr(entity, "url"): + entity_url = str(entity.url) if entity.url else None + elif hasattr(entity, "profile_url"): + entity_url = str(entity.profile_url) if entity.profile_url else None + + if not entity_url: + display_name = "Unknown" + if entity: + if hasattr(entity, "title"): + display_name = entity.title or "Unknown" + elif hasattr(entity, "name"): + display_name = entity.name or "Unknown" + logger.warning( + f"Skipping relation without URL: {display_name}, " + f"entity_type={type(entity).__name__ if entity else 'None'}", + ) + continue + + # Extract UUID from entity (handles both Pydantic models and dicts) + entity_uuid = None + if hasattr(entity, "uuid"): + entity_uuid = entity.uuid + elif isinstance(entity, dict): + entity_uuid = entity.get("uuid") + + # For Infoscience, UUID is mandatory + if not entity_uuid: + display_name = "Unknown" + if entity: + if hasattr(entity, "title"): + display_name = entity.title or "Unknown" + elif hasattr(entity, "name"): + display_name = entity.name or "Unknown" + logger.warning( + f"Skipping Infoscience relation without UUID: {display_name}", + ) + continue + + try: + # Prepare expected entity data based on entity type + expected_entity = {} + if relation.entityType == EntityType.PUBLICATION: + if hasattr(entity, "title"): + expected_entity["title"] = entity.title + expected_entity["authors"] = getattr(entity, "authors", []) + expected_entity["doi"] = getattr(entity, "doi", None) + expected_entity["publication_date"] = getattr( + entity, + "publication_date", + None, + ) + expected_entity["lab"] = getattr(entity, "lab", None) + elif isinstance(entity, dict): + expected_entity = entity + elif relation.entityType == EntityType.PERSON: + if hasattr(entity, "name"): + expected_entity["name"] = entity.name + expected_entity["affiliation"] = getattr( + entity, + "affiliation", + None, + ) + expected_entity["orcid"] = getattr(entity, "orcid", None) + expected_entity["email"] = getattr(entity, "email", None) + elif isinstance(entity, dict): + expected_entity = entity + elif relation.entityType == EntityType.ORGUNIT: + if hasattr(entity, "name"): + expected_entity["name"] = entity.name + expected_entity["parent_organization"] = getattr( + entity, + "parent_organization", + None, + ) + expected_entity["description"] = getattr( + entity, + "description", + None, + ) + elif isinstance(entity, dict): + expected_entity = entity + + # Validate Infoscience URL + validation_result = await validate_infoscience_url( + url=str(entity_url), + expected_entity=expected_entity, + entity_type=relation.entityType.value, + ctx=None, + ) + + display_name = "Unknown" + if entity: + if hasattr(entity, "title"): + display_name = entity.title or "Unknown" + elif hasattr(entity, "name"): + display_name = entity.name or "Unknown" + + if not validation_result.is_valid: + logger.warning( + f"⚠ Infoscience validation failed for {display_name}: " + f"{validation_result.justification}", + ) + # Skip invalid relation + continue + + # Update URL if normalized + if ( + validation_result.normalized_url + and validation_result.normalized_url != entity_url + ): + logger.info( + f"✓ Normalized Infoscience URL: {entity_url} -> {validation_result.normalized_url}", + ) + # Update entity URL + if hasattr(entity, "url"): + entity.url = validation_result.normalized_url + elif hasattr(entity, "profile_url"): + entity.profile_url = validation_result.normalized_url + + # Update confidence based on validation + if validation_result.confidence < 0.6: + logger.info( + f"⚠ Low confidence Infoscience match for {display_name}: " + f"confidence={validation_result.confidence:.2f}", + ) + # Reduce relation confidence + relation.confidence = min( + relation.confidence, + validation_result.confidence, + ) + else: + logger.info( + f"✓ Infoscience validation passed for {display_name}: " + f"confidence={validation_result.confidence:.2f}", + ) + + validated_relations.append(relation) + + except Exception as e: + display_name = "Unknown" + if entity: + if hasattr(entity, "title"): + display_name = entity.title or "Unknown" + elif hasattr(entity, "name"): + display_name = entity.name or "Unknown" + logger.error( + f"Error validating Infoscience URL for {display_name}: {e}", + exc_info=True, + ) + # Skip relation on error + continue + + return validated_relations + + +async def enrich_repository_linked_entities( + repository_url: str, + repository_name: str, + description: str, + readme_excerpt: str, + authors: list = None, + organizations: list = None, + force_refresh: bool = False, +) -> dict: + """ + Enrich repository with linked entities relations. + + Args: + repository_url: URL of the repository + repository_name: Name of the repository + description: Repository description + readme_excerpt: Excerpt from README + authors: List of identified author names + organizations: List of identified organization names + + Returns: + Dictionary with 'data' (linkedEntitiesEnrichmentResult) and 'usage' keys + """ + prompt = get_repository_linked_entities_prompt( + repository_url=repository_url, + repository_name=repository_name, + description=description, + readme_excerpt=readme_excerpt, + authors=authors or [], + organizations=organizations or [], + ) + + logger.info( + f"🔍 Starting linked entities enrichment for repository: {repository_name}", + ) + + # Clear Infoscience cache if force_refresh is True + if force_refresh: + clear_infoscience_cache() + + try: + result = await run_agent_with_fallback(linked_entities_configs, prompt) + + if result and result.get("data"): + enrichment_data = result["data"] + + # Validate Infoscience URLs in repository relations + logger.info("🔍 Validating Infoscience URLs in repository relations...") + enrichment_data.repository_relations = ( + await _validate_infoscience_relations( + enrichment_data.repository_relations, + ) + ) + + logger.info( + f"✓ Found {len(enrichment_data.repository_relations)} validated repository relations", + ) + + # Validate Infoscience URLs in author relations + if hasattr(enrichment_data, "author_relations"): + logger.info("🔍 Validating Infoscience URLs in author relations...") + for author_name, relations in enrichment_data.author_relations.items(): + enrichment_data.author_relations[ + author_name + ] = await _validate_infoscience_relations( + relations, + ) + logger.info( + f"✓ Validated author relations for {len(enrichment_data.author_relations)} authors", + ) + + # Validate Infoscience URLs in organization relations + if hasattr(enrichment_data, "organization_relations"): + logger.info( + "🔍 Validating Infoscience URLs in organization relations...", + ) + for ( + org_name, + relations, + ) in enrichment_data.organization_relations.items(): + enrichment_data.organization_relations[ + org_name + ] = await _validate_infoscience_relations( + relations, + ) + logger.info( + f"✓ Validated organization relations for {len(enrichment_data.organization_relations)} organizations", + ) + + return result + except Exception as e: + logger.error(f"linked entities enrichment failed: {e}") + # Return empty result instead of failing + return { + "data": linkedEntitiesEnrichmentResult( + repository_relations=[], + searchStrategy="Enrichment failed", + totalSearches=0, + ), + "usage": {"input_tokens": 0, "output_tokens": 0}, + } + + +async def enrich_user_linked_entities( + username: str, + full_name: str, + bio: str, + organizations: list, + force_refresh: bool = False, +) -> dict: + """ + Enrich user with linked entities relations. + + Args: + username: GitHub username + full_name: User's full name + bio: User's bio + organizations: List of organizations + + Returns: + Dictionary with 'data' (linkedEntitiesEnrichmentResult) and 'usage' keys + """ + prompt = get_user_linked_entities_prompt( + username=username, + full_name=full_name, + bio=bio, + organizations=organizations, + ) + + logger.info(f"🔍 Starting linked entities enrichment for user: {username}") + + # Clear Infoscience cache if force_refresh is True + if force_refresh: + clear_infoscience_cache() + + try: + result = await run_agent_with_fallback(linked_entities_configs, prompt) + + if result and result.get("data"): + enrichment_data = result["data"] + + # Validate Infoscience URLs in author relations + logger.info("🔍 Validating Infoscience URLs in author relations...") + for author_name, relations in enrichment_data.author_relations.items(): + enrichment_data.author_relations[ + author_name + ] = await _validate_infoscience_relations( + relations, + ) + + logger.info( + f"✓ Found linked entities relations for {len(enrichment_data.author_relations)} authors", + ) + + return result + except Exception as e: + logger.error(f"linked entities enrichment failed: {e}") + # Return empty result instead of failing + return { + "data": linkedEntitiesEnrichmentResult( + author_relations={}, + searchStrategy="Enrichment failed", + totalSearches=0, + ), + "usage": {"input_tokens": 0, "output_tokens": 0}, + } + + +async def enrich_organization_linked_entities( + org_name: str, + description: str, + website: str, + members: list, + force_refresh: bool = False, +) -> dict: + """ + Enrich organization with linked entities relations. + + Args: + org_name: Organization name + description: Organization description + website: Organization website + members: List of member usernames + + Returns: + Dictionary with 'data' (linkedEntitiesEnrichmentResult) and 'usage' keys + """ + prompt = get_organization_linked_entities_prompt( + org_name=org_name, + description=description, + website=website, + members=members, + ) + + logger.info(f"🔍 Starting linked entities enrichment for organization: {org_name}") + + # Clear Infoscience cache if force_refresh is True + if force_refresh: + clear_infoscience_cache() + + try: + result = await run_agent_with_fallback(linked_entities_configs, prompt) + + if result and result.get("data"): + enrichment_data = result["data"] + + # Validate Infoscience URLs in organization relations + logger.info("🔍 Validating Infoscience URLs in organization relations...") + for org_name, relations in enrichment_data.organization_relations.items(): + enrichment_data.organization_relations[ + org_name + ] = await _validate_infoscience_relations( + relations, + ) + + logger.info( + f"✓ Found linked entities relations for {len(enrichment_data.organization_relations)} organizations", + ) + + return result + except Exception as e: + logger.error(f"linked entities enrichment failed: {e}") + # Return empty result instead of failing + return { + "data": linkedEntitiesEnrichmentResult( + organization_relations={}, + searchStrategy="Enrichment failed", + totalSearches=0, + ), + "usage": {"input_tokens": 0, "output_tokens": 0}, + } diff --git a/src/agents/academic_catalog_prompts.py b/src/agents/linked_entities_prompts.py similarity index 59% rename from src/agents/academic_catalog_prompts.py rename to src/agents/linked_entities_prompts.py index bfe7b1e..66d2307 100644 --- a/src/agents/academic_catalog_prompts.py +++ b/src/agents/linked_entities_prompts.py @@ -1,11 +1,11 @@ """ -Prompts for Academic Catalog Enrichment Agent +Prompts for linked entities Enrichment Agent This agent is responsible for finding and linking entities to academic catalogs (Infoscience, OpenAlex, EPFL Graph, etc.) """ -academic_catalog_system_prompt = """ +linked_entities_system_prompt = """ You are an expert at searching academic catalogs and matching entities to publications, authors, and organizational units. @@ -86,18 +86,26 @@ 1. **Extract UUID** - Look for "*UUID:* " in the markdown output - This is REQUIRED for creating proper catalog links - The UUID appears after the name in the format "*UUID:* " + - Example: "*UUID:* 0469064e-5977-4569-93a2-522b6d758e50" -2. **Extract URL** - The profile/publication URL from the markdown link - - Format: **[Name](https://infoscience.epfl.ch/entities/...)** +2. **Extract URL** - Look for "*URL:* " in the markdown output + - The URL is explicitly listed as "*URL:* https://infoscience.epfl.ch/entities/..." + - URL formats: + - Publications: `https://infoscience.epfl.ch/entities/publication/{uuid}` + - Persons: `https://infoscience.epfl.ch/entities/person/{uuid}` + - OrgUnits: `https://infoscience.epfl.ch/entities/orgunit/{uuid}` + - The URL is also in the markdown link format: **[Name](url)**, but use the explicit "*URL:*" field + - **REQUIRED**: You MUST include the URL in the entity object 3. **Extract all available fields**: - - For **persons**: name, UUID, email, ORCID, affiliation, profile_url - - For **orgunits**: name, UUID, description, url, parent_organization, website, research_areas - - For **publications**: title, UUID, authors, DOI, publication_date, url, abstract + - For **persons**: name, UUID, email, ORCID, affiliation, profile_url (use the URL from "*URL:*" field) + - For **orgunits**: name, UUID, description, url (use the URL from "*URL:*" field), parent_organization, website, research_areas + - For **publications**: title, UUID, authors, DOI, publication_date, url (use the URL from "*URL:*" field), abstract 4. **Parse structured data from markdown**: - Each field is on its own line with format "*Field:* value" - Parse each field carefully to build complete entity objects + - **URL is REQUIRED** - if you find a UUID, you can construct the URL: `https://infoscience.epfl.ch/entities/{type}/{uuid}` ## Justification @@ -106,9 +114,17 @@ - Why it's related (matching fields, shared authors, etc.) - What makes you confident (exact match, multiple sources, etc.) +**IMPORTANT**: A publication is only related to an organization if the organization or one of its members is directly involved (e.g., as an author or in the affiliations). Do not relate a publication just because the topic is relevant. + ## Output Format -Return an `AcademicCatalogEnrichmentResult` with **organized relations**: +**IMPORTANT - Data Types:** +- All URLs must be **strings** (e.g., "https://infoscience.epfl.ch/entities/publication/...") +- Do NOT use HttpUrl objects or special URL types +- Dates should be strings in ISO format (YYYY-MM-DD or YYYY) +- All fields should use primitive types: strings, numbers, lists, dictionaries + +Return an `linkedEntitiesEnrichmentResult` with **organized relations**: - **repository_relations**: Publications/entities related to the repository itself (searched by repository name) - **author_relations**: Dictionary keyed by author name (as provided), each containing their person profile + publications @@ -127,38 +143,41 @@ ```json { "repository_relations": [ - {"entityType": "publication", "entity": {...}, "confidence": 0.95} + {"entityType": "publication", "entityInfosciencePublication": {"title": "...", "uuid": "..."}, "confidence": 0.95} ], "author_relations": { "Alexander Mathis": [ - {"entityType": "person", "entity": {...}, "confidence": 0.95}, - {"entityType": "publication", "entity": {...}, "confidence": 0.9} + {"entityType": "person", "entityInfoscienceAuthor": {"name": "Alexander Mathis", "uuid": "..."}, "confidence": 0.95}, + {"entityType": "publication", "entityInfosciencePublication": {"title": "...", "uuid": "..."}, "confidence": 0.9} ], "Mackenzie Weygandt Mathis": [ - {"entityType": "person", "entity": {...}, "confidence": 0.95} + {"entityType": "person", "entityInfoscienceAuthor": {"name": "Mackenzie Weygandt Mathis", "uuid": "..."}, "confidence": 0.95} ] }, "organization_relations": { "DeepLabCut": [ - {"entityType": "orgunit", "entity": {...}, "confidence": 0.8} + {"entityType": "orgunit", "entityInfoscienceOrgUnit": {"name": "DeepLabCut", "uuid": "..."}, "confidence": 0.8} ] } } ``` -Each `AcademicCatalogRelation` should have: +Each `linkedEntitiesRelation` should have: - **catalogType**: "infoscience" (more catalogs will be added in the future) - **entityType**: "publication", "person", or "orgunit" -- **uuid**: Extract from markdown ("*UUID:* ") - REQUIRED! -- **url**: Extract from markdown link ([Name](url)) -- **name**: Entity name/title -- **entity**: The full entity object with ALL available fields from the markdown: - - For **person**: {uuid, name, email, orcid, affiliation, profile_url} - - For **orgunit**: {uuid, name, description, url, parent_organization, website, research_areas} - - For **publication**: {uuid, title, authors, abstract, doi, publication_date, publication_type, url, lab, subjects} +- **entity field**: Based on the `entityType`, you must populate ONE of the following fields with the full entity object. The object should contain ALL available fields from the markdown: + - `entityInfosciencePublication`: If `entityType` is "publication". Use fields: {uuid, title, authors, abstract, doi, publication_date, publication_type, url, lab, subjects} where url is the URL from "*URL:*" field + - `entityInfoscienceAuthor`: If `entityType` is "person". Use fields: {uuid, name, email, orcid, affiliation, profile_url} where profile_url is the URL from "*URL:*" field + - `entityInfoscienceOrgUnit`: If `entityType` is "orgunit". Use fields: {uuid, name, description, url, parent_organization, website, research_areas} where url is the URL from "*URL:*" field - **confidence**: Your confidence score (0.0-1.0) - **justification**: Clear explanation of the match -- **matchedOn**: List of fields used for matching (e.g., ["name", "email"], ["doi"]) + +**URL Construction Rules:** +- If you have a UUID, you can construct the URL: `https://infoscience.epfl.ch/entities/{entityType}/{uuid}` +- For publications: `https://infoscience.epfl.ch/entities/publication/{uuid}` +- For persons: `https://infoscience.epfl.ch/entities/person/{uuid}` +- For orgunits: `https://infoscience.epfl.ch/entities/orgunit/{uuid}` +- Always include both the top-level `url` field AND the entity's URL field (url or profile_url) ## Important Notes @@ -172,7 +191,7 @@ """ -def get_repository_academic_catalog_prompt( +def get_repository_linked_entities_prompt( repository_url: str, repository_name: str, description: str, @@ -181,7 +200,7 @@ def get_repository_academic_catalog_prompt( organizations: list = None, ) -> str: """ - Generate prompt for repository academic catalog enrichment. + Generate prompt for repository linked entities enrichment. Args: repository_url: URL of the repository @@ -194,11 +213,26 @@ def get_repository_academic_catalog_prompt( Returns: Formatted prompt for the agent """ - authors_str = ", ".join(authors) if authors else "None identified yet" - orgs_str = ", ".join(organizations) if organizations else "None identified yet" + # Truncate lists to prevent token overflow + max_items = 50 + authors_list = authors[:max_items] if authors else [] + if authors and len(authors) > max_items: + authors_list.append(f"... and {len(authors) - max_items} more") + + orgs_list = organizations[:max_items] if organizations else [] + if organizations and len(organizations) > max_items: + orgs_list.append(f"... and {len(organizations) - max_items} more") + + authors_str = ", ".join(authors_list) if authors_list else "None identified yet" + orgs_str = ", ".join(orgs_list) if orgs_list else "None identified yet" + + # Truncate README + readme_content = readme_excerpt or "No README available" + if len(readme_content) > 5000: + readme_content = readme_content[:5000] + "... (truncated)" return f""" -## Repository Academic Catalog Enrichment +## Repository linked entities Enrichment **Repository**: {repository_url} **Name**: {repository_name} @@ -208,7 +242,7 @@ def get_repository_academic_catalog_prompt( **README excerpt**: ``` -{readme_excerpt or "No README available"} +{readme_content} ``` ## Your Task @@ -257,18 +291,18 @@ def get_repository_academic_catalog_prompt( - Academic profiles may use variations like "Mathis, Alexander" or "Alexander Mathis" - that's fine, the matching happens later - If no results for an author/org, return empty list for that key -Return your findings as an `AcademicCatalogEnrichmentResult` with the organized structure. +Return your findings as an `linkedEntitiesEnrichmentResult` with the organized structure. """ -def get_user_academic_catalog_prompt( +def get_user_linked_entities_prompt( username: str, full_name: str, bio: str, organizations: list, ) -> str: """ - Generate prompt for user academic catalog enrichment. + Generate prompt for user linked entities enrichment. Args: username: GitHub username @@ -279,13 +313,25 @@ def get_user_academic_catalog_prompt( Returns: Formatted prompt for the agent """ + # Truncate organizations list + max_items = 50 + orgs_list = organizations[:max_items] if organizations else [] + if organizations and len(organizations) > max_items: + orgs_list.append(f"... and {len(organizations) - max_items} more") + + orgs_str = ", ".join(orgs_list) if orgs_list else "None" + + # Truncate bio + bio_content = bio or "Not provided" + if len(bio_content) > 2000: + bio_content = bio_content[:2000] + "... (truncated)" return f""" -## User Academic Catalog Enrichment +## User linked entities Enrichment **GitHub Username**: {username} **Full Name**: {full_name or "Not provided"} -**Bio**: {bio or "Not provided"} -**Organizations**: {", ".join(organizations) if organizations else "None"} +**Bio**: {bio_content} +**Organizations**: {orgs_str} ## Your Task @@ -302,6 +348,10 @@ def get_user_academic_catalog_prompt( - Note their research areas 3. **Organizational affiliations** - Find their lab or research group + - **IMPORTANT**: When searching for orgunit (labs), include the user's name in the search query + - Some labs use GitHub user profiles, so searching with both lab name and user name helps find them + - For each organization, try: `search_infoscience_labs_tool("{{org_name}} {full_name}")` or `search_infoscience_labs_tool("{full_name}")` + - Also try searching with just the organization name: `search_infoscience_labs_tool("{{org_name}}")` - Check publication metadata for labs - Look for institutional affiliations - Match with bio information @@ -310,23 +360,27 @@ def get_user_academic_catalog_prompt( 1. If full name available: `search_infoscience_authors_tool("{full_name}")` 2. If authors found: `get_author_publications_tool("{full_name}")` -3. Check publications for lab/organizational information -4. Search for labs mentioned in bio +3. For each organization, search for orgunit: + - Try: `search_infoscience_labs_tool("{{org_name}} {full_name}")` (lab name + user name) + - Try: `search_infoscience_labs_tool("{full_name}")` (user name only - labs sometimes use GitHub profiles) + - Try: `search_infoscience_labs_tool("{{org_name}}")` (organization name only) +4. Check publications for lab/organizational information +5. Search for labs mentioned in bio Remember: Not all GitHub users are academic researchers. If no results, that's okay. -Return your findings as an `AcademicCatalogEnrichmentResult`. +Return your findings as an `linkedEntitiesEnrichmentResult`. """ -def get_organization_academic_catalog_prompt( +def get_organization_linked_entities_prompt( org_name: str, description: str, website: str, members: list, ) -> str: """ - Generate prompt for organization academic catalog enrichment. + Generate prompt for organization linked entities enrichment. Args: org_name: Organization name @@ -338,7 +392,7 @@ def get_organization_academic_catalog_prompt( Formatted prompt for the agent """ return f""" -## Organization Academic Catalog Enrichment +## Organization linked entities Enrichment **Organization Name**: {org_name} **Description**: {description or "Not provided"} @@ -350,12 +404,18 @@ def get_organization_academic_catalog_prompt( Search academic catalogs to find entities related to this organization: 1. **Organizational Unit** - Search for this organization as a lab or research group - - Search for the organization name: "{org_name}" + - **IMPORTANT**: Search with MULTIPLE name variations: + * Start with the organization name: "{org_name}" + * Also try the full organization name if different (e.g., from description: "{description[:100] if description else 'N/A'}") + * Try acronyms or short names (e.g., if description mentions "SDSC", also search for "SDSC") + * Try "Swiss Data Science Center" if the org name is "sdsc-ordes" or similar + * Try any alternative names or variations found in the description - Look for EPFL affiliations - Find related labs or departments + - **If one search returns no results, try another variation** 2. **Publications** - Find publications from this organization - - Search by organization name + - Search by organization name with MULTIPLE variations (same as above) - Look for papers with this affiliation - Check for research outputs @@ -366,12 +426,24 @@ def get_organization_academic_catalog_prompt( ## Search Instructions +**CRITICAL - Try Multiple Name Variations:** + 1. Start with: `search_infoscience_labs_tool("{org_name}")` -2. Then: `search_infoscience_publications_tool("{org_name}")` -3. If relevant authors identified, search for them -4. Cross-reference findings +2. If no results, try full name variations: + - Extract full name from description if available + - Try acronyms (e.g., "SDSC" for "Swiss Data Science Center") + - Try "Swiss Data Science Center" if org name is "sdsc-ordes" +3. Then: `search_infoscience_publications_tool("{org_name}")` and also try with name variations +4. If relevant authors identified, search for them +5. Cross-reference findings + +**Example**: For "sdsc-ordes", try: +- `search_infoscience_labs_tool("sdsc-ordes")` +- `search_infoscience_labs_tool("SDSC")` +- `search_infoscience_labs_tool("Swiss Data Science Center")` +- Same variations for publications search Remember: Not all GitHub organizations are academic. Commercial organizations may not have entries. -Return your findings as an `AcademicCatalogEnrichmentResult`. +Return your findings as an `linkedEntitiesEnrichmentResult`. """ diff --git a/src/agents/organization_enrichment.py b/src/agents/organization_enrichment.py index 74fb8f8..d7f097c 100644 --- a/src/agents/organization_enrichment.py +++ b/src/agents/organization_enrichment.py @@ -33,7 +33,6 @@ search_infoscience_labs_tool, search_infoscience_publications_tool, ) -from .url_validation import validate_ror_url from ..data_models import ( GitAuthor, OrganizationAnalysisContext, @@ -52,6 +51,7 @@ get_organization_enrichment_prompt, organization_enrichment_main_system_prompt, ) +from .url_validation import validate_ror_url # Configure logging logger = logging.getLogger(__name__) @@ -95,7 +95,9 @@ async def validate_ror_organization_tool( Returns: JSON string with validation result """ - logger.info(f"🔍 Agent tool called: validate_ror_organization('{ror_id}', '{org_name}')") + logger.info( + f"🔍 Agent tool called: validate_ror_organization('{ror_id}', '{org_name}')", + ) try: # Prepare expected org dict expected_org = { @@ -131,7 +133,7 @@ async def validate_ror_organization_tool( { "is_valid": False, "confidence": 0.0, - "justification": f"Error during validation: {str(e)}", + "justification": f"Error during validation: {e!s}", "matched_fields": [], "validation_errors": [str(e)], }, @@ -248,12 +250,16 @@ async def search_ror( # Fallback to first name value if not org_name and item.get("names"): org_name = item.get("names", [{}])[0].get("value") - + # Extract country from locations if country field is not available country = item.get("country", {}).get("country_name") if not country and item.get("locations"): - country = item.get("locations", [{}])[0].get("geonames_details", {}).get("country_name") - + country = ( + item.get("locations", [{}])[0] + .get("geonames_details", {}) + .get("country_name") + ) + org_info = { "name": org_name, "ror_id": item.get("id"), @@ -262,8 +268,14 @@ async def search_ror( "aliases": item.get("aliases", []), "acronyms": item.get("acronyms", []), "links": item.get("links", []), - "names": item.get("names", []), # Include full names array for agent - "locations": item.get("locations", []), # Include locations for country info + "names": item.get( + "names", + [], + ), # Include full names array for agent + "locations": item.get( + "locations", + [], + ), # Include locations for country info "relationships": [ { "label": rel.get("label"), @@ -279,17 +291,25 @@ async def search_ror( # Log the actual results for debugging (INFO level so agent can see what it has) logger.info(f"📋 ROR search results for '{query}':") for i, result in enumerate(results[:5], 1): # Show top 5 - ror_id_clean = result.get("ror_id", "").split("/")[-1] if "/" in result.get("ror_id", "") else result.get("ror_id", "") + ror_id_clean = ( + result.get("ror_id", "").split("/")[-1] + if "/" in result.get("ror_id", "") + else result.get("ror_id", "") + ) logger.info( - f" {i}. {result.get('name', 'N/A')} - ROR ID: {ror_id_clean}" + f" {i}. {result.get('name', 'N/A')} - ROR ID: {ror_id_clean}", ) - + # DEBUG: Show EXACTLY what we're returning to the agent json_result = json.dumps(results, indent=2) - logger.info(f"🔍 DEBUG - EXACT ROR SEARCH RESULT PROVIDED TO AGENT for '{query}':") + logger.info( + f"🔍 DEBUG - EXACT ROR SEARCH RESULT PROVIDED TO AGENT for '{query}':", + ) logger.info(f"JSON returned to agent ({len(json_result)} chars):") logger.info(json_result) - logger.debug(f"Full ROR search results for '{query}': {json.dumps(results, indent=2)}") + logger.debug( + f"Full ROR search results for '{query}': {json.dumps(results, indent=2)}", + ) return json_result except Exception as e: @@ -710,27 +730,40 @@ async def _pre_search_ror_for_organizations( """ Proactively search ROR for organizations identified from ORCID affiliations and existing mentions. Does NOT search for email domains - let the agent decide on those. - + Returns: Dictionary mapping organization names/queries to their ROR search results """ ror_results = {} organizations_to_search = set() - + # Extract from ORCID affiliations for author in context.authors: if author.affiliations: for aff in author.affiliations: - if aff and aff.strip(): - organizations_to_search.add(aff.strip()) - + if isinstance(aff, dict) and aff.get("name"): + # Handle dict representation (from model_dump) + org_name = aff.get("name") + if org_name and org_name.strip(): + organizations_to_search.add(org_name.strip()) + elif hasattr(aff, "name"): + # Handle Affiliation object + if aff.name and aff.name.strip(): + organizations_to_search.add(aff.name.strip()) + elif isinstance(aff, str): + # Handle legacy string format (should not occur) + if aff and aff.strip(): + organizations_to_search.add(aff.strip()) + # Add existing organization mentions for org in context.existing_organizations: if org and org.strip(): organizations_to_search.add(org.strip()) - - logger.info(f"🔍 Pre-searching ROR for {len(organizations_to_search)} organizations (from ORCID and existing mentions)...") - + + logger.info( + f"🔍 Pre-searching ROR for {len(organizations_to_search)} organizations (from ORCID and existing mentions)...", + ) + # Search ROR for each organization for org_query in organizations_to_search: try: @@ -744,7 +777,7 @@ async def _pre_search_ror_for_organizations( ) response.raise_for_status() data = response.json() - + # Process results (same logic as search_ror tool) results = [] for item in data.get("items", [])[:5]: @@ -762,12 +795,16 @@ async def _pre_search_ror_for_organizations( break if not org_name and item.get("names"): org_name = item.get("names", [{}])[0].get("value") - + # Extract country from locations if country field is not available country = item.get("country", {}).get("country_name") if not country and item.get("locations"): - country = item.get("locations", [{}])[0].get("geonames_details", {}).get("country_name") - + country = ( + item.get("locations", [{}])[0] + .get("geonames_details", {}) + .get("country_name") + ) + # Extract only essential fields for affiliation matching # Limit to parent relationships only (most relevant for affiliation) parent_relationships = [ @@ -779,7 +816,7 @@ async def _pre_search_ror_for_organizations( for rel in item.get("relationships", []) if rel.get("type") == "parent" ][:2] # Limit to 2 parent relationships max - + # Extract website from links website = None if item.get("links"): @@ -787,7 +824,7 @@ async def _pre_search_ror_for_organizations( if link.get("type") == "website": website = link.get("value") break - + # Extract key aliases (limit to 3 most important) key_aliases = [] if item.get("names"): @@ -798,7 +835,7 @@ async def _pre_search_ror_for_organizations( key_aliases.append(alias) if len(key_aliases) >= 3: break - + org_info = { "name": org_name, "ror_id": item.get("id"), @@ -808,16 +845,18 @@ async def _pre_search_ror_for_organizations( "parent_organizations": parent_relationships, # Only parent relationships } results.append(org_info) - + if results: ror_results[org_query] = results - logger.info(f" ✓ Found {len(results)} ROR results for '{org_query}'") + logger.info( + f" ✓ Found {len(results)} ROR results for '{org_query}'", + ) else: logger.info(f" ⚠ No ROR results for '{org_query}'") - + except Exception as e: logger.warning(f" ✗ Error pre-searching ROR for '{org_query}': {e}") - + logger.info(f"✅ Pre-searched ROR for {len(ror_results)} organizations") return ror_results @@ -856,28 +895,40 @@ async def enrich_organizations( ) # Pre-search ROR for organizations from ORCID and existing mentions - logger.info("🔍 Pre-searching ROR for organizations from ORCID affiliations and existing mentions...") + logger.info( + "🔍 Pre-searching ROR for organizations from ORCID affiliations and existing mentions...", + ) pre_searched_ror = await _pre_search_ror_for_organizations(context) - + # DEBUG: Log what we found if pre_searched_ror: - logger.info(f"📋 Pre-searched ROR results: {len(pre_searched_ror)} organizations") + logger.info( + f"📋 Pre-searched ROR results: {len(pre_searched_ror)} organizations", + ) for org_query, results in pre_searched_ror.items(): logger.info(f" '{org_query}': {len(results)} ROR matches") for i, result in enumerate(results[:3], 1): - ror_id = result.get("ror_id", "").split("/")[-1] if "/" in result.get("ror_id", "") else result.get("ror_id", "") + ror_id = ( + result.get("ror_id", "").split("/")[-1] + if "/" in result.get("ror_id", "") + else result.get("ror_id", "") + ) logger.info(f" {i}. {result.get('name')} - ROR: {ror_id}") else: logger.info(" No organizations found to pre-search") - + # Prepare the prompt for the agent (include pre-searched ROR results) - prompt = get_organization_enrichment_prompt(repository_url, context, pre_searched_ror) + prompt = get_organization_enrichment_prompt( + repository_url, + context, + pre_searched_ror, + ) logger.info(f"🚀 Starting organization enrichment for {repository_url}") logger.info( f"📊 Input data: {len(context.git_authors)} git authors, {len(context.authors)} ORCID authors", ) - + # Run the agent with fallback across multiple models logger.info("🤖 Running PydanticAI agent with fallback...") result = await run_agent_with_fallback(org_enrichment_configs, prompt, context) @@ -950,7 +1001,7 @@ async def enrich_organizations( org_name = org.legalName if hasattr(org, "legalName") else str(org) ror_id = str(org.hasRorId) if org.hasRorId else "None" logger.info(f" {i}. {org_name} - ROR ID: {ror_id}") - + # Validate ROR IDs for all organizations logger.info("🔍 Validating ROR IDs for all organizations...") for org in result.output.organizations: @@ -962,7 +1013,7 @@ async def enrich_organizations( ror_id = ror_id.split("/")[-1] logger.info( - f"🔍 Validating ROR ID for {org.legalName}: {ror_id} (URL: https://ror.org/{ror_id})" + f"🔍 Validating ROR ID for {org.legalName}: {ror_id} (URL: https://ror.org/{ror_id})", ) # Quick pre-validation: Check if ROR ID exists in ROR API @@ -973,18 +1024,18 @@ async def enrich_organizations( if api_response.status_code == 404: logger.warning( f"⚠ ROR ID {ror_id} does not exist in ROR API (404). " - f"Removing invalid ROR ID for {org.legalName}" + f"Removing invalid ROR ID for {org.legalName}", ) org.hasRorId = None continue elif api_response.status_code != 200: logger.warning( f"⚠ ROR API returned {api_response.status_code} for {ror_id}. " - f"Proceeding with full validation..." + f"Proceeding with full validation...", ) except Exception as e: logger.debug( - f"Pre-validation check failed for {ror_id}: {e}. Proceeding with full validation..." + f"Pre-validation check failed for {ror_id}: {e}. Proceeding with full validation...", ) # Prepare expected org data @@ -1006,14 +1057,14 @@ async def enrich_organizations( if not validation_result.is_valid: logger.warning( f"⚠ ROR validation failed for {org.legalName} (ROR: {ror_id}): " - f"{validation_result.justification}" + f"{validation_result.justification}", ) # Remove invalid ROR ID org.hasRorId = None elif validation_result.confidence < 0.7: logger.info( f"⚠ Low confidence ROR match for {org.legalName} (ROR: {ror_id}): " - f"confidence={validation_result.confidence:.2f}" + f"confidence={validation_result.confidence:.2f}", ) # Reduce confidence if org.attributionConfidence: @@ -1021,7 +1072,7 @@ async def enrich_organizations( else: logger.info( f"✓ ROR validation passed for {org.legalName} (ROR: {ror_id}): " - f"confidence={validation_result.confidence:.2f}" + f"confidence={validation_result.confidence:.2f}", ) except Exception as e: @@ -1138,28 +1189,40 @@ async def enrich_organizations_from_dict( ) # Pre-search ROR for organizations from ORCID and existing mentions - logger.info("🔍 Pre-searching ROR for organizations from ORCID affiliations and existing mentions...") + logger.info( + "🔍 Pre-searching ROR for organizations from ORCID affiliations and existing mentions...", + ) pre_searched_ror = await _pre_search_ror_for_organizations(context) - + # DEBUG: Log what we found if pre_searched_ror: - logger.info(f"📋 Pre-searched ROR results: {len(pre_searched_ror)} organizations") + logger.info( + f"📋 Pre-searched ROR results: {len(pre_searched_ror)} organizations", + ) for org_query, results in pre_searched_ror.items(): logger.info(f" '{org_query}': {len(results)} ROR matches") for i, result in enumerate(results[:3], 1): - ror_id = result.get("ror_id", "").split("/")[-1] if "/" in result.get("ror_id", "") else result.get("ror_id", "") + ror_id = ( + result.get("ror_id", "").split("/")[-1] + if "/" in result.get("ror_id", "") + else result.get("ror_id", "") + ) logger.info(f" {i}. {result.get('name')} - ROR: {ror_id}") else: logger.info(" No organizations found to pre-search") - + # Prepare the prompt for the agent (include pre-searched ROR results) - prompt = get_organization_enrichment_prompt(repository_url, context, pre_searched_ror) + prompt = get_organization_enrichment_prompt( + repository_url, + context, + pre_searched_ror, + ) logger.info(f"🚀 Starting organization enrichment from dict for {repository_url}") logger.info( f"📊 Input data: {len(git_authors)} git authors, {len(authors)} ORCID authors", ) - + # Run the agent with fallback across multiple models logger.info("🤖 Running PydanticAI agent with fallback...") result = await run_agent_with_fallback(org_enrichment_configs, prompt, context) @@ -1232,7 +1295,7 @@ async def enrich_organizations_from_dict( org_name = org.legalName if hasattr(org, "legalName") else str(org) ror_id = str(org.hasRorId) if org.hasRorId else "None" logger.info(f" {i}. {org_name} - ROR ID: {ror_id}") - + # Pydantic Validation if OrganizationEnrichmentResult.model_validate(result.output): logger.info("✅ Output validated against OrganizationEnrichmentResult model") @@ -1243,7 +1306,7 @@ async def enrich_organizations_from_dict( # Validate ROR IDs for all organizations enriched_result = OrganizationEnrichmentResult(**result.output.model_dump()) - + logger.info("🔍 Validating ROR IDs for all organizations...") for org in enriched_result.organizations: if org.hasRorId: @@ -1254,7 +1317,7 @@ async def enrich_organizations_from_dict( ror_id = ror_id.split("/")[-1] logger.info( - f"🔍 Validating ROR ID for {org.legalName}: {ror_id} (URL: https://ror.org/{ror_id})" + f"🔍 Validating ROR ID for {org.legalName}: {ror_id} (URL: https://ror.org/{ror_id})", ) # Quick pre-validation: Check if ROR ID exists in ROR API @@ -1265,18 +1328,18 @@ async def enrich_organizations_from_dict( if api_response.status_code == 404: logger.warning( f"⚠ ROR ID {ror_id} does not exist in ROR API (404). " - f"Removing invalid ROR ID for {org.legalName}" + f"Removing invalid ROR ID for {org.legalName}", ) org.hasRorId = None continue elif api_response.status_code != 200: logger.warning( f"⚠ ROR API returned {api_response.status_code} for {ror_id}. " - f"Proceeding with full validation..." + f"Proceeding with full validation...", ) except Exception as e: logger.debug( - f"Pre-validation check failed for {ror_id}: {e}. Proceeding with full validation..." + f"Pre-validation check failed for {ror_id}: {e}. Proceeding with full validation...", ) # Prepare expected org data @@ -1298,14 +1361,14 @@ async def enrich_organizations_from_dict( if not validation_result.is_valid: logger.warning( f"⚠ ROR validation failed for {org.legalName} (ROR: {ror_id}): " - f"{validation_result.justification}" + f"{validation_result.justification}", ) # Remove invalid ROR ID org.hasRorId = None elif validation_result.confidence < 0.7: logger.info( f"⚠ Low confidence ROR match for {org.legalName} (ROR: {ror_id}): " - f"confidence={validation_result.confidence:.2f}" + f"confidence={validation_result.confidence:.2f}", ) # Reduce confidence if org.attributionConfidence: @@ -1313,7 +1376,7 @@ async def enrich_organizations_from_dict( else: logger.info( f"✓ ROR validation passed for {org.legalName} (ROR: {ror_id}): " - f"confidence={validation_result.confidence:.2f}" + f"confidence={validation_result.confidence:.2f}", ) except Exception as e: diff --git a/src/agents/organization_prompts.py b/src/agents/organization_prompts.py index 89756a7..ee5a57c 100644 --- a/src/agents/organization_prompts.py +++ b/src/agents/organization_prompts.py @@ -110,7 +110,7 @@ """ import json -from typing import Optional, Dict, Any +from typing import Any, Dict, Optional ####################################### # Organization Enrichment Prompt General @@ -118,9 +118,9 @@ def get_organization_enrichment_prompt( - repository_url: str, - context, - pre_searched_ror: Optional[Dict[str, Any]] = None + repository_url: str, + context, + pre_searched_ror: Optional[Dict[str, Any]] = None, ) -> str: # Format pre-searched ROR results pre_searched_ror_section = "" @@ -132,7 +132,7 @@ def get_organization_enrichment_prompt( {json.dumps(pre_searched_ror, indent=2)} -**IMPORTANT**: +**IMPORTANT**: - For organizations listed above, select the BEST matching ROR entry from the pre-searched results - For email domains (e.g., @epfl.ch, @ethz.ch), use the search_ror tool to find the organization - For any other organizations you identify, use the search_ror tool if needed @@ -142,7 +142,7 @@ def get_organization_enrichment_prompt( **PRE-SEARCHED ROR DATA:** No organizations were pre-searched. Use the search_ror tool for all organizations you identify. """ - + prompt = f"""Analyze the following repository metadata and identify all related organizations. Repository: {repository_url} @@ -177,7 +177,18 @@ def get_organization_enrichment_prompt( { "name": a.name, "orcid": str(a.orcid) if a.orcid else None, - "affiliations": a.affiliations, + "affiliations": [ + { + "name": aff.name, + "organizationId": aff.organizationId, + "source": aff.source, + } + if hasattr(aff, "name") + else aff + for aff in a.affiliations + ] + if a.affiliations + else [], } for a in context.authors ], @@ -207,7 +218,7 @@ def get_organization_enrichment_prompt( # Log token breakdown for debugging from ..utils.token_counter import estimate_tokens_from_messages - + # Estimate tokens for each section git_authors_json = json.dumps( [ @@ -228,39 +239,56 @@ def get_organization_enrichment_prompt( ], indent=2, ) - + orcid_authors_json = json.dumps( [ { "name": a.name, "orcid": str(a.orcid) if a.orcid else None, - "affiliations": a.affiliations, + "affiliations": [ + { + "name": aff.name, + "organizationId": aff.organizationId, + "source": aff.source, + } + if hasattr(aff, "name") + else aff + for aff in a.affiliations + ] + if a.affiliations + else [], } for a in context.authors ], indent=2, ) - - pre_searched_ror_json = json.dumps(pre_searched_ror, indent=2) if pre_searched_ror else "" - + + pre_searched_ror_json = ( + json.dumps(pre_searched_ror, indent=2) if pre_searched_ror else "" + ) + # Estimate tokens for each section system_tokens = estimate_tokens_from_messages( system_prompt=organization_enrichment_main_system_prompt, user_prompt="", ).get("input_tokens", 0) - + git_authors_tokens = estimate_tokens_from_messages( user_prompt=git_authors_json, ).get("input_tokens", 0) - + orcid_authors_tokens = estimate_tokens_from_messages( user_prompt=orcid_authors_json, ).get("input_tokens", 0) - - pre_searched_ror_tokens = estimate_tokens_from_messages( - user_prompt=pre_searched_ror_json, - ).get("input_tokens", 0) if pre_searched_ror_json else 0 - + + pre_searched_ror_tokens = ( + estimate_tokens_from_messages( + user_prompt=pre_searched_ror_json, + ).get("input_tokens", 0) + if pre_searched_ror_json + else 0 + ) + rest_of_prompt = f"""Analyze the following repository metadata and identify all related organizations. Repository: {repository_url} @@ -280,23 +308,36 @@ def get_organization_enrichment_prompt( [Instructions section...] """ - + rest_tokens = estimate_tokens_from_messages( user_prompt=rest_of_prompt, ).get("input_tokens", 0) - - total_estimated = system_tokens + git_authors_tokens + orcid_authors_tokens + pre_searched_ror_tokens + rest_tokens - + + total_estimated = ( + system_tokens + + git_authors_tokens + + orcid_authors_tokens + + pre_searched_ror_tokens + + rest_tokens + ) + import logging + logger = logging.getLogger(__name__) - logger.info(f"🔍 PROMPT TOKEN BREAKDOWN (estimated):") + logger.info("🔍 PROMPT TOKEN BREAKDOWN (estimated):") logger.info(f" System prompt: ~{system_tokens:,} tokens") - logger.info(f" Git authors JSON: ~{git_authors_tokens:,} tokens ({len(context.git_authors)} authors)") - logger.info(f" ORCID authors JSON: ~{orcid_authors_tokens:,} tokens ({len(context.authors)} authors)") - logger.info(f" Pre-searched ROR data: ~{pre_searched_ror_tokens:,} tokens ({len(pre_searched_ror) if pre_searched_ror else 0} organizations)") + logger.info( + f" Git authors JSON: ~{git_authors_tokens:,} tokens ({len(context.git_authors)} authors)", + ) + logger.info( + f" ORCID authors JSON: ~{orcid_authors_tokens:,} tokens ({len(context.authors)} authors)", + ) + logger.info( + f" Pre-searched ROR data: ~{pre_searched_ror_tokens:,} tokens ({len(pre_searched_ror) if pre_searched_ror else 0} organizations)", + ) logger.info(f" Rest of prompt: ~{rest_tokens:,} tokens") logger.info(f" TOTAL ESTIMATED: ~{total_estimated:,} tokens") - + return prompt diff --git a/src/agents/repository.py b/src/agents/repository.py index 6026f56..df26ef2 100644 --- a/src/agents/repository.py +++ b/src/agents/repository.py @@ -15,7 +15,9 @@ load_model_config, validate_config, ) -from ..utils.token_counter import estimate_tokens_from_messages +from ..utils.token_counter import ( + estimate_tokens_with_tools, +) from ..utils.url_validation import ( validate_and_clean_urls, validate_author_urls, @@ -144,14 +146,11 @@ async def llm_request_repo_infos( elif isinstance(json_data, str): response_text = json_data - estimated = estimate_tokens_from_messages( - system_prompt=system_prompt_repository, - user_prompt=prompt, - response=response_text, - ) - # Extract usage information from the result usage_data = None + input_tokens = 0 + output_tokens = 0 + tool_calls_count = 0 if hasattr(result, "usage"): usage = result.usage @@ -159,6 +158,7 @@ async def llm_request_repo_infos( # First try to get tokens from direct attributes input_tokens = getattr(usage, "input_tokens", 0) or 0 output_tokens = getattr(usage, "output_tokens", 0) or 0 + tool_calls_count = getattr(usage, "tool_calls", 0) or 0 # If tokens are 0, check the details field (for Anthropic, OpenAI reasoning models, etc.) # See: https://github.com/pydantic/pydantic-ai/issues/3223 @@ -170,28 +170,40 @@ async def llm_request_repo_infos( logger.debug( f"Extracted tokens from usage.details: input={input_tokens}, output={output_tokens}", ) - - usage_data = { - "input_tokens": input_tokens, - "output_tokens": output_tokens, - "estimated_input_tokens": estimated.get("input_tokens", 0), - "estimated_output_tokens": estimated.get("output_tokens", 0), - } - logger.info( - f"Repository agent token usage - Input: {input_tokens}, Output: {output_tokens}", - ) - logger.info( - f"Repository agent estimated - Input: {estimated.get('input_tokens', 0)}, Output: {estimated.get('output_tokens', 0)}", - ) else: logger.warning("Result object has no 'usage' attribute") - # Use estimates as fallback - usage_data = { - "input_tokens": 0, - "output_tokens": 0, - "estimated_input_tokens": estimated.get("input_tokens", 0), - "estimated_output_tokens": estimated.get("output_tokens", 0), - } + + # Calculate estimates with tool call support (always, for validation/fallback) + estimated = estimate_tokens_with_tools( + system_prompt=system_prompt_repository, + user_prompt=prompt, + response=response_text, + tool_calls=tool_calls_count, + tool_results_text=None, # Tool results text extraction would require access to all_messages + ) + + # Use estimates as primary when API returns 0 + if input_tokens == 0 and output_tokens == 0: + logger.warning( + "API returned 0 tokens, using tiktoken estimates as primary counts", + ) + input_tokens = estimated.get("input_tokens", 0) + output_tokens = estimated.get("output_tokens", 0) + + usage_data = { + "input_tokens": input_tokens, + "output_tokens": output_tokens, + "estimated_input_tokens": estimated.get("input_tokens", 0), + "estimated_output_tokens": estimated.get("output_tokens", 0), + } + logger.info( + f"Repository agent token usage - Input: {input_tokens}, Output: {output_tokens}", + ) + logger.info( + f"Repository agent estimated - Input: {estimated.get('input_tokens', 0)}, Output: {estimated.get('output_tokens', 0)}", + ) + if tool_calls_count > 0: + logger.info(f"Repository agent tool calls: {tool_calls_count}") # Ensure it's a dictionary if hasattr(json_data, "model_dump"): diff --git a/src/agents/url_validation.py b/src/agents/url_validation.py index bedd33d..f1b5b21 100644 --- a/src/agents/url_validation.py +++ b/src/agents/url_validation.py @@ -154,7 +154,9 @@ async def validate_ror_url( Returns: ValidationResult """ - logger.info(f"Validating ROR URL: {ror_id} for organization: {expected_org.get('name', 'Unknown')}") + logger.info( + f"Validating ROR URL: {ror_id} for organization: {expected_org.get('name', 'Unknown')}", + ) # Extract ROR ID from URL if needed if ror_id.startswith("http://") or ror_id.startswith("https://"): @@ -166,7 +168,7 @@ async def validate_ror_url( ror_id_clean = ror_id ror_api_url = f"https://api.ror.org/v2/organizations/{ror_id_clean}" logger.debug(f"Using ROR ID '{ror_id_clean}' directly (not a full URL)") - + logger.debug(f"Fetching ROR data from API: {ror_api_url}") try: @@ -175,7 +177,7 @@ async def validate_ror_url( response = await client.get(ror_api_url) response.raise_for_status() ror_data = response.json() - + # Extract organization name from names array org_name = None if ror_data.get("names"): @@ -188,19 +190,23 @@ async def validate_ror_url( if "label" in name_entry.get("types", []): org_name = name_entry.get("value") break - + # Extract country from locations country = None if ror_data.get("locations"): - country = ror_data.get("locations", [{}])[0].get("geonames_details", {}).get("country_name") - + country = ( + ror_data.get("locations", [{}])[0] + .get("geonames_details", {}) + .get("country_name") + ) + # Extract aliases aliases = [] if ror_data.get("names"): for name_entry in ror_data.get("names", []): if name_entry.get("value") != org_name: aliases.append(name_entry.get("value")) - + # Extract website website = None if ror_data.get("links"): @@ -208,7 +214,7 @@ async def validate_ror_url( if link.get("type") == "website": website = link.get("value") break - + # Format ROR data for validation prompt ror_data_summary = f"""ROR API Data: - Name: {org_name or 'N/A'} @@ -270,7 +276,7 @@ async def validate_ror_url( is_valid=False, confidence=0.0, justification=f"ROR ID {ror_id_clean} does not exist in ROR API (404 error)", - validation_errors=[f"HTTP 404: ROR ID not found"], + validation_errors=["HTTP 404: ROR ID not found"], ) else: logger.error(f"HTTP error fetching ROR API data: {e.response.status_code}") @@ -285,7 +291,7 @@ async def validate_ror_url( return ValidationResult( is_valid=False, confidence=0.0, - justification=f"Error during validation: {str(e)}", + justification=f"Error during validation: {e!s}", validation_errors=[str(e)], ) @@ -453,7 +459,6 @@ async def validate_infoscience_url( return ValidationResult( is_valid=False, confidence=0.0, - justification=f"Error during validation: {str(e)}", + justification=f"Error during validation: {e!s}", validation_errors=[str(e)], ) - diff --git a/src/agents/user_prompts.py b/src/agents/user_prompts.py index 91c0c17..f8614f9 100644 --- a/src/agents/user_prompts.py +++ b/src/agents/user_prompts.py @@ -27,6 +27,12 @@ * Temporal alignment between commit dates and known affiliation periods * Amount and recency of contribution to the repository +**Affiliation Structure:** +Each affiliation must be returned as an object with: +- "name": Organization name (required, e.g., "Swiss Data Science Center", "EPFL") +- "organizationId": ROR ID, GitHub handle, or internal ID (optional, null if unknown) +- "source": Data source (required, one of: "orcid", "github_profile", "email_domain", "agent_user_enrichment") + Pay special attention to: - Different name variations (e.g., "John Smith", "J. Smith", "Smith, John") - Institutional email domains (e.g., @epfl.ch, @ethz.ch, @university.edu) @@ -91,7 +97,18 @@ def get_user_enrichment_agent_prompt(repository_url: str, context: UserAnalysisC { "name": a.name, "orcid": str(a.orcid) if a.orcid else None, - "affiliations": a.affiliations, + "affiliations": [ + { + "name": aff.name, + "organizationId": aff.organizationId, + "source": aff.source, + } + if hasattr(aff, "name") + else aff + for aff in a.affiliations + ] + if a.affiliations + else [], } for a in context.existing_authors ], @@ -141,31 +158,33 @@ def get_general_user_agent_prompt(username: str, user_data: str): - "positionJustification": List of justifications for each position IMPORTANT: Extract organization and position information ONLY from the actual data provided: - - Company field: "{user_data.get('company', 'N/A')}" - - Bio content: "{user_data.get('bio', 'N/A')}" - - README content: "{user_data.get('readme_content', 'N/A')[:500]}..." (truncated) - - Organization affiliations: {user_data.get('organizations', [])} - - ORCID activities: {user_data.get('orcid_activities', 'N/A')} + - Company field: "{user_data.get('company') or 'N/A'}" + - Bio content: "{user_data.get('bio') or 'N/A'}" + - README content: "{(user_data.get('readme_content') or 'N/A')[:500]}..." (truncated) + - Organization affiliations: {user_data.get('organizations') or []} + - Repositories: {user_data.get('repositories') or []} + - ORCID activities: {user_data.get('orcid_activities') or 'N/A'} EXTRACTION GUIDELINES: - + **For Positions:** - Look for explicit statements about current or past roles in the bio, company field, or README - Look for phrases like "I am working as", "Currently working as", "Software Engineer at", etc. - - ONLY extract positions that are EXPLICITLY mentioned in the data - - DO NOT infer or assume positions that are not stated - + - If explicit positions are found, extract them. + - **INFERENCE ALLOWED:** If NO explicit position is found, but the user has > 10 public repositories or > 100 followers (see provided data), you MAY infer "Open Source Developer" or "Software Engineer" based on the repositories' languages and content. + - If inferring, state clearly in the justification that it is inferred from GitHub activity. + **For Organizations:** - Look for company/employer information in the bio, company field, and README - Check GitHub organizations the user is a member of (institutions, universities, companies) - Include both primary organizations (e.g., "EPFL") and sub-units (e.g., "Swiss Data Science Center") ONLY if mentioned - Add EPFL to the list ONLY if the user explicitly mentions affiliation with an EPFL lab/center or has @epfl.ch email - DO NOT add organizations that are not explicitly mentioned or clearly indicated - + **For Disciplines:** - Infer from the user's bio, projects, repositories, and stated roles - Base on technical skills, research areas, or explicit statements - + **Critical Rules:** - For each field, provide a clear justification that quotes or references the actual source data - If a field cannot be determined from the available data, return an empty list [] diff --git a/src/agents/validation_utils.py b/src/agents/validation_utils.py index 0da7e6a..ee18245 100644 --- a/src/agents/validation_utils.py +++ b/src/agents/validation_utils.py @@ -80,7 +80,9 @@ async def fetch_html_content(url: str, use_selenium: bool = True) -> str: # Convert to markdown to preserve structure markdown_content = md(str(soup), heading_style="ATX", bullets="-") - logger.info(f"✓ Fetched HTML content from {url} using Selenium (converted to markdown)") + logger.info( + f"✓ Fetched HTML content from {url} using Selenium (converted to markdown)", + ) return markdown_content finally: @@ -111,7 +113,9 @@ async def fetch_html_content(url: str, use_selenium: bool = True) -> str: # Convert to markdown to preserve structure markdown_content = md(str(soup), heading_style="ATX", bullets="-") - logger.info(f"✓ Fetched HTML content from {url} using httpx (converted to markdown)") + logger.info( + f"✓ Fetched HTML content from {url} using httpx (converted to markdown)", + ) return markdown_content except Exception as e: @@ -141,7 +145,11 @@ def normalize_infoscience_url(url_or_uuid: str, entity_type: str) -> Optional[st uuid = uuid_match.group(1) else: # Check if it's just a UUID - if re.match(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", url_or_uuid, re.IGNORECASE): + if re.match( + r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", + url_or_uuid, + re.IGNORECASE, + ): uuid = url_or_uuid else: # Not a valid UUID format @@ -175,4 +183,3 @@ def normalize_infoscience_author_url(url_or_uuid: str) -> Optional[str]: def normalize_infoscience_lab_url(url_or_uuid: str) -> Optional[str]: """Normalize an Infoscience lab/orgunit URL.""" return normalize_infoscience_url(url_or_uuid, "orgunit") - diff --git a/src/analysis/organization.py b/src/analysis/organization.py index 84b2c04..529581b 100644 --- a/src/analysis/organization.py +++ b/src/analysis/organization.py @@ -2,8 +2,8 @@ from datetime import datetime from ..agents import llm_request_org_infos -from ..agents.academic_catalog_enrichment import enrich_organization_academic_catalog from ..agents.epfl_assessment import assess_epfl_relationship +from ..agents.linked_entities_enrichment import enrich_organization_linked_entities from ..agents.organization_enrichment import enrich_organizations_from_dict from ..cache.cache_manager import CacheManager, get_cache_manager from ..data_models import GitHubOrganization @@ -47,6 +47,7 @@ def run_github_parsing(self): # Map GitHubOrganizationMetadata fields to GitHubOrganization model self.data = GitHubOrganization( # Basic fields + id=f"https://github.com/{self.org_name}", # Full GitHub organization URL name=org_data_dict.get("name"), githubOrganizationMetadata=github_metadata, # Enrichment fields (will be populated by analysis steps) @@ -63,6 +64,340 @@ def run_github_parsing(self): infoscienceEntities=None, ) + async def run_atomic_llm_pipeline(self): + """ + Run atomic LLM pipeline for organization analysis. + + Stages: + 1. Context Compilation - Gather organization info using tools (Infoscience, web search) + 2. Structured Output - Extract basic identity fields (name, description) + 3. Classification - Classify organizationType and discipline with justifications + 4. Organization Identifier - Identify related organizations (parent, partner, affiliated orgs) + 5. Linked Entities - Search Infoscience for orgunit and related publications + 6. EPFL Assessment - Final holistic EPFL relationship assessment + """ + logger.info(f"Starting atomic LLM pipeline for {self.org_name}") + + if self.data is None: + logger.error("Cannot run atomic LLM pipeline: no data available") + return + + # Prepare GitHub metadata + github_metadata = ( + self.data.githubOrganizationMetadata.model_dump() + if self.data.githubOrganizationMetadata + else {} + ) + + org_url = f"https://github.com/{self.org_name}" + + # Stage 1: Compile organization context + logger.info("Stage 1: Compiling organization context...") + from ..agents.atomic_agents.organization_context_compiler import ( + compile_organization_context, + ) + + compiled_result = await compile_organization_context( + org_name=self.org_name, + org_url=org_url, + github_metadata=github_metadata, + ) + + compiled_context = compiled_result.get("data") + usage = compiled_result.get("usage") + + if not compiled_context: + logger.error("Organization context compilation failed") + return + + # Accumulate usage from context compiler + if usage: + self.total_input_tokens += usage.get("input_tokens", 0) + self.total_output_tokens += usage.get("output_tokens", 0) + if "estimated_input_tokens" in usage: + self.estimated_input_tokens += usage.get("estimated_input_tokens", 0) + self.estimated_output_tokens += usage.get("estimated_output_tokens", 0) + + # Log Stage 1 token usage + logger.info("=" * 80) + logger.info("STAGE 1 (Organization Context Compiler) Token Usage:") + logger.info( + f" Input tokens: {usage.get('input_tokens', 0):,} (official) | {usage.get('estimated_input_tokens', 0):,} (estimated)", + ) + logger.info( + f" Output tokens: {usage.get('output_tokens', 0):,} (official) | {usage.get('estimated_output_tokens', 0):,} (estimated)", + ) + logger.info( + f" Total tokens: {usage.get('input_tokens', 0) + usage.get('output_tokens', 0):,}", + ) + logger.info("=" * 80) + + # Stage 2: Generate structured output + logger.info("Stage 2: Generating structured output...") + # Import the simplified model (it's generated at module level in organization_structured_output) + from ..agents.atomic_agents.organization_structured_output import ( + _SIMPLIFIED_MODEL, + generate_organization_structured_output, + ) + + # Generate schema from the simplified model's JSON schema + schema = _SIMPLIFIED_MODEL.model_json_schema() + # Create a minimal example for reference + example = { + "name": "Example Organization", + "description": "Example organization description", + } + + structured_result = await generate_organization_structured_output( + compiled_context=compiled_context, + schema=schema, + example=example, + ) + + structured_output = structured_result.get("data") + usage = structured_result.get("usage") + + if not structured_output: + logger.error("Organization structured output generation failed") + return + + # Accumulate usage from structured output + if usage: + self.total_input_tokens += usage.get("input_tokens", 0) + self.total_output_tokens += usage.get("output_tokens", 0) + if "estimated_input_tokens" in usage: + self.estimated_input_tokens += usage.get("estimated_input_tokens", 0) + self.estimated_output_tokens += usage.get("estimated_output_tokens", 0) + + # Log Stage 2 token usage + logger.info("=" * 80) + logger.info("STAGE 2 (Organization Structured Output) Token Usage:") + logger.info( + f" Input tokens: {usage.get('input_tokens', 0):,} (official) | {usage.get('estimated_input_tokens', 0):,} (estimated)", + ) + logger.info( + f" Output tokens: {usage.get('output_tokens', 0):,} (official) | {usage.get('estimated_output_tokens', 0):,} (estimated)", + ) + logger.info( + f" Total tokens: {usage.get('input_tokens', 0) + usage.get('output_tokens', 0):,}", + ) + logger.info("=" * 80) + + # Convert simplified output to dict + if hasattr(structured_output, "model_dump"): + simplified_dict = structured_output.model_dump() + else: + simplified_dict = structured_output + + # Merge basic fields into self.data + if simplified_dict.get("name"): + self.data.name = simplified_dict.get("name") + if simplified_dict.get("description"): + self.data.description = simplified_dict.get("description") + + # Stage 3: Classify organization type and discipline + logger.info("Stage 3: Classifying organization type and discipline...") + from ..agents.atomic_agents.organization_classifier import ( + classify_organization_type_and_discipline, + ) + + classification_result = await classify_organization_type_and_discipline( + compiled_context=compiled_context, + ) + + classification = classification_result.get("data") + usage = classification_result.get("usage") + + if not classification: + logger.error("Organization classification failed") + return + + # Accumulate usage from classification + if usage: + self.total_input_tokens += usage.get("input_tokens", 0) + self.total_output_tokens += usage.get("output_tokens", 0) + if "estimated_input_tokens" in usage: + self.estimated_input_tokens += usage.get("estimated_input_tokens", 0) + self.estimated_output_tokens += usage.get("estimated_output_tokens", 0) + + # Log Stage 3 token usage + logger.info("=" * 80) + logger.info("STAGE 3 (Organization Classifier) Token Usage:") + logger.info( + f" Input tokens: {usage.get('input_tokens', 0):,} (official) | {usage.get('estimated_input_tokens', 0):,} (estimated)", + ) + logger.info( + f" Output tokens: {usage.get('output_tokens', 0):,} (official) | {usage.get('estimated_output_tokens', 0):,} (estimated)", + ) + logger.info( + f" Total tokens: {usage.get('input_tokens', 0) + usage.get('output_tokens', 0):,}", + ) + logger.info("=" * 80) + + # Convert classification to dict + if hasattr(classification, "model_dump"): + classification_dict = classification.model_dump() + else: + classification_dict = classification + + # Update organizationType and discipline fields + if classification_dict.get("organizationType"): + self.data.organizationType = classification_dict.get("organizationType") + if classification_dict.get("organizationTypeJustification"): + self.data.organizationTypeJustification = classification_dict.get( + "organizationTypeJustification", + ) + if classification_dict.get("discipline"): + # Convert discipline strings to Discipline enum values + from ..data_models.models import Discipline + + discipline_strings = classification_dict.get("discipline", []) + discipline_enums = [] + for disc_str in discipline_strings: + try: + # Try to find matching Discipline enum by value + discipline_enum = Discipline(disc_str) + discipline_enums.append(discipline_enum) + except ValueError: + logger.warning(f"Unknown discipline value: {disc_str}, skipping") + continue + + if discipline_enums: + self.data.discipline = discipline_enums + if classification_dict.get("disciplineJustification"): + self.data.disciplineJustification = classification_dict.get( + "disciplineJustification", + [], + ) + + logger.info( + f"Organization classified as '{classification_dict.get('organizationType', 'unknown')}' " + f"with {len(classification_dict.get('discipline', []))} discipline(s)", + ) + + # Stage 4: Identify related organizations + logger.info("Stage 4: Identifying related organizations...") + from ..agents.atomic_agents.organization_identifier import ( + identify_related_organizations, + ) + from ..data_models.models import Organization + + organization_result = await identify_related_organizations( + compiled_context=compiled_context, + context_type="organization", + ) + + organization_data = organization_result.get("data") + usage = organization_result.get("usage") + + if not organization_data: + logger.error("Organization identification failed") + return + + # Accumulate usage from organization identification + if usage: + self.total_input_tokens += usage.get("input_tokens", 0) + self.total_output_tokens += usage.get("output_tokens", 0) + if "estimated_input_tokens" in usage: + self.estimated_input_tokens += usage.get("estimated_input_tokens", 0) + self.estimated_output_tokens += usage.get("estimated_output_tokens", 0) + + # Log Stage 4 token usage + logger.info("=" * 80) + logger.info("STAGE 4 (Organization Identifier) Token Usage:") + logger.info( + f" Input tokens: {usage.get('input_tokens', 0):,} (official) | {usage.get('estimated_input_tokens', 0):,} (estimated)", + ) + logger.info( + f" Output tokens: {usage.get('output_tokens', 0):,} (official) | {usage.get('estimated_output_tokens', 0):,} (estimated)", + ) + logger.info( + f" Total tokens: {usage.get('input_tokens', 0) + usage.get('output_tokens', 0):,}", + ) + logger.info("=" * 80) + + # Convert organization data to dict + if hasattr(organization_data, "model_dump"): + organization_dict = organization_data.model_dump() + else: + organization_dict = organization_data + + # Convert SimplifiedOrganization objects to full Organization objects + organizations = [] + for org_data in organization_dict.get("relatedToOrganizations", []): + # Convert dict to Organization if needed + if isinstance(org_data, dict): + # Map SimplifiedOrganization.name to Organization.legalName + if "name" in org_data and "legalName" not in org_data: + org_data["legalName"] = org_data.pop("name") + + # Ensure type is set + if "type" not in org_data: + org_data["type"] = "Organization" + # Set source if not present + if "source" not in org_data: + org_data["source"] = "atomic_agent" + # Ensure id is set (use legalName as fallback if no id) + if "id" not in org_data or not org_data["id"]: + org_data["id"] = org_data.get("legalName", "") + + try: + org = Organization(**org_data) + organizations.append(org) + except Exception as e: + logger.warning(f"Failed to create Organization from dict: {e}") + continue + elif isinstance(org_data, Organization): + organizations.append(org_data) + else: + logger.warning( + f"Unexpected organization data type: {type(org_data)}", + ) + + if organizations: + # Merge with existing organizations (avoid duplicates) + existing_orgs = self.data.relatedToOrganization or [] + existing_names = set() + for org in existing_orgs: + if isinstance(org, Organization): + existing_names.add(org.legalName.lower() if org.legalName else "") + elif isinstance(org, str): + existing_names.add(org.lower()) + + for org in organizations: + org_name = org.legalName.lower() if org.legalName else "" + if org_name not in existing_names: + existing_orgs.append(org) + existing_names.add(org_name) + + self.data.relatedToOrganization = existing_orgs + logger.info( + f"Identified {len(organizations)} related organizations (total: {len(existing_orgs)})", + ) + + # Update justifications + if organization_dict.get("relatedToOrganizationJustification"): + existing_justifications = self.data.relatedToOrganizationJustification or [] + new_justifications = organization_dict.get( + "relatedToOrganizationJustification", + [], + ) + # Merge justifications (avoid duplicates) + for justification in new_justifications: + if justification not in existing_justifications: + existing_justifications.append(justification) + self.data.relatedToOrganizationJustification = existing_justifications + + # Stage 5: Linked entities enrichment + logger.info("Stage 5: Searching linked entities (orgunit and publications)...") + await self.run_linked_entities_enrichment() + + # Stage 6: EPFL assessment + logger.info("Stage 6: Running EPFL final assessment...") + await self.run_epfl_final_assessment() + + logger.info("Atomic LLM pipeline completed successfully") + async def run_llm_analysis(self): """Run LLM analysis to populate organizationType and discipline fields""" logger.info(f"LLM analysis for {self.org_name}") @@ -96,7 +431,7 @@ async def run_llm_analysis(self): result = await llm_request_org_infos( org_name=self.org_name, org_data=llm_input_data, - max_tokens=20000, + max_tokens=10000, ) # Extract data and usage @@ -359,7 +694,7 @@ async def run_organization_enrichment(self): logger.info(f"Organization enrichment completed for {self.org_name}") - async def run_academic_catalog_enrichment(self): + async def run_linked_entities_enrichment(self): """Enrich organization with academic catalog relations (Infoscience, etc.)""" logger.info(f"Academic catalog enrichment for {self.org_name}") @@ -384,11 +719,12 @@ async def run_academic_catalog_enrichment(self): website = github_metadata.get("blog", "") members = github_metadata.get("public_members", []) - result = await enrich_organization_academic_catalog( + result = await enrich_organization_linked_entities( org_name=self.org_name, description=description, website=website, members=members, + force_refresh=self.force_refresh, ) # Extract data and usage @@ -410,7 +746,7 @@ async def run_academic_catalog_enrichment(self): # Store the academic catalog relations if enrichment_data and hasattr(enrichment_data, "relations"): - self.data.academicCatalogRelations = enrichment_data.relations + self.data.linkedEntities = enrichment_data.relations logger.info( f"Stored {len(enrichment_data.relations)} academic catalog relations", ) @@ -562,9 +898,9 @@ def dump_results(self, output_type="json") -> str | dict | None: if output_type == "pydantic": return self.data elif output_type == "json": - return self.data.model_dump_json(indent=2) + return self.data.model_dump_json(indent=2, exclude_none=True) elif output_type == "dict": - return self.data.model_dump() + return self.data.model_dump(exclude_none=True) else: logging.error(f"Unsupported output type: {output_type}") return None @@ -599,11 +935,11 @@ async def run_analysis( self.run_github_parsing() logging.info(f"GitHub parsing completed for {self.org_name}") - # Run LLM analysis + # Run atomic LLM pipeline if run_llm: - logging.info(f"LLM analysis for {self.org_name}") - await self.run_llm_analysis() - logging.info(f"LLM analysis completed for {self.org_name}") + logging.info(f"Atomic LLM pipeline for {self.org_name}") + await self.run_atomic_llm_pipeline() + logging.info(f"Atomic LLM pipeline completed for {self.org_name}") # Run organization enrichment if run_organization_enrichment: @@ -611,17 +947,8 @@ async def run_analysis( await self.run_organization_enrichment() logging.info(f"Organization enrichment completed for {self.org_name}") - # Run academic catalog enrichment - if self.data is not None: - logging.info(f"Academic catalog enrichment for {self.org_name}") - await self.run_academic_catalog_enrichment() - logging.info(f"Academic catalog enrichment completed for {self.org_name}") - - # Run final EPFL assessment after all enrichments complete - if self.data is not None: - logging.info(f"Final EPFL assessment for {self.org_name}") - await self.run_epfl_final_assessment() - logging.info(f"Final EPFL assessment completed for {self.org_name}") + # Note: Linked entities enrichment and EPFL assessment are already included + # in the atomic LLM pipeline (Stages 5 and 6), so we don't need to run them again here # Validate and cache if we have data if self.data is not None: diff --git a/src/analysis/repositories.py b/src/analysis/repositories.py index 5677b84..df3b33f 100644 --- a/src/analysis/repositories.py +++ b/src/analysis/repositories.py @@ -1,14 +1,24 @@ import logging from datetime import datetime - -from ..agents.academic_catalog_enrichment import enrich_repository_academic_catalog -from ..agents.epfl_assessment import assess_epfl_relationship +from typing import Optional + +from ..agents.atomic_agents import ( + assess_final_epfl_relationship, + classify_repository_type_and_discipline, + compile_enriched_data_for_epfl, + compile_repository_context, + generate_structured_output, + identify_related_organizations, + search_academic_catalogs, + structure_linked_entities, +) from ..agents.organization_enrichment import enrich_organizations_from_dict -from ..agents.repository import llm_request_repo_infos from ..agents.user_enrichment import enrich_users_from_dict from ..cache.cache_manager import CacheManager, get_cache_manager -from ..data_models import Organization, SoftwareSourceCode +from ..context import prepare_repository_context +from ..data_models import Affiliation, Organization, SoftwareSourceCode from ..gimie_utils.gimie_methods import extract_gimie +from ..utils.url_validation import normalize_orcid_id from ..utils.utils import enrich_authors_with_orcid logging.basicConfig(level=logging.INFO) @@ -47,6 +57,29 @@ def __init__(self, full_path: str, force_refresh: bool = False): f"Cannot process repository: {full_path} is not public or not accessible", ) + @staticmethod + def _normalize_orcid(orcid_value): + """Normalize ORCID to canonical ID format, dropping invalid values.""" + return normalize_orcid_id(orcid_value) + + @classmethod + def _sanitize_person_payload(cls, payload: dict, context: str) -> dict: + """Sanitize a Person payload dict before Person model validation.""" + if not isinstance(payload, dict): + return payload + + sanitized_payload = payload.copy() + if "orcid" in sanitized_payload: + original_orcid = sanitized_payload.get("orcid") + normalized_orcid = cls._normalize_orcid(original_orcid) + if original_orcid and normalized_orcid is None: + logger.warning( + f"Dropping invalid ORCID in {context}: {original_orcid}", + ) + sanitized_payload["orcid"] = normalized_orcid + + return sanitized_payload + def run_gimie_analysis(self): def fetch_gimie_data(): return extract_gimie(self.full_path, format="json-ld") @@ -66,44 +99,2062 @@ def fetch_gimie_data(): self.gimie = jsonld_gimie_data async def run_llm_analysis(self): - result = await llm_request_repo_infos( - str(self.full_path), - gimie_output=self.gimie, - max_tokens=20000, + """Run LLM analysis using the atomic agent pipeline.""" + await self.run_atomic_llm_pipeline() + + async def run_atomic_llm_pipeline(self): + """ + Run atomic agent pipeline: context compilation -> structured output -> EPFL check. + + This implements a two-stage pipeline: + 1. Context compiler: Gathers repository information using tools + 2. Structured output: Produces structured metadata from compiled context + 3. EPFL checker: Assesses EPFL relationship from compiled context + """ + logger.info(f"Starting atomic LLM pipeline for {self.full_path}") + + # Prepare repository context (clone, extract content, etc.) + context_result = await prepare_repository_context( + self.full_path, + max_tokens=40000, ) - # Extract data and usage - llm_data = result.get("data") if isinstance(result, dict) else result - usage = result.get("usage") if isinstance(result, dict) else None + if not context_result["success"]: + logger.error( + f"Failed to prepare repository context: {context_result.get('error')}", + ) + return - # Accumulate official API-reported usage data + repository_content = context_result["input_text"] + git_authors = context_result.get("git_authors", []) + + logger.info( + f"Repository content prepared: {len(repository_content):,} chars, {len(git_authors)} git authors", + ) + + # Extract structured authors and organizations from GIMIE + gimie_authors_orgs = self._extract_gimie_authors_and_organizations() + + # Prepare GIMIE data as string if available + gimie_data = None + if self.gimie: + import json as json_module + + # Include structured authors/orgs in GIMIE data for context compiler + # Convert Pydantic models to dicts for JSON serialization + authors_list = gimie_authors_orgs.get("authors", []) + orgs_list = gimie_authors_orgs.get("organizations", []) + + gimie_data_dict = { + "raw_gimie": self.gimie, + "extracted_authors": [ + a.model_dump() if hasattr(a, "model_dump") else a + for a in authors_list + ], + "extracted_organizations": [ + o.model_dump() if hasattr(o, "model_dump") else o for o in orgs_list + ], + } + + gimie_data = json_module.dumps(gimie_data_dict, indent=2, default=str) + logger.info( + f"GIMIE data prepared: {len(gimie_authors_orgs.get('authors', []))} authors, " + f"{len(gimie_authors_orgs.get('organizations', []))} organizations, " + f"{len(gimie_data):,} chars", + ) + else: + logger.warning("No GIMIE data available for context compiler") + + # Stage 1: Compile repository context + logger.info("Stage 1: Compiling repository context...") + compiled_result = await compile_repository_context( + repo_url=self.full_path, + repository_content=repository_content, + gimie_data=gimie_data, + git_authors=git_authors, + ) + + compiled_context = compiled_result.get("data") + usage = compiled_result.get("usage") + + if not compiled_context: + logger.error("Context compilation failed") + return + + # Accumulate usage from context compiler if usage: self.total_input_tokens += usage.get("input_tokens", 0) self.total_output_tokens += usage.get("output_tokens", 0) + if "estimated_input_tokens" in usage: + self.estimated_input_tokens += usage.get("estimated_input_tokens", 0) + self.estimated_output_tokens += usage.get("estimated_output_tokens", 0) + + # Log Stage 1 token usage + logger.info("=" * 80) + logger.info("STAGE 1 (Context Compiler) Token Usage:") logger.info( - f"LLM analysis usage: {usage.get('input_tokens', 0)} input, {usage.get('output_tokens', 0)} output tokens", + f" Input tokens: {usage.get('input_tokens', 0):,} (official) | {usage.get('estimated_input_tokens', 0):,} (estimated)", ) + logger.info( + f" Output tokens: {usage.get('output_tokens', 0):,} (official) | {usage.get('estimated_output_tokens', 0):,} (estimated)", + ) + logger.info( + f" Total tokens: {usage.get('input_tokens', 0) + usage.get('output_tokens', 0):,}", + ) + logger.info("=" * 80) + + # Stage 2: Generate structured output + logger.info("Stage 2: Generating structured output...") + # Get simplified schema from the dynamically generated model + # Import the simplified model (it's generated at module level in structured_output) + from ..agents.atomic_agents.structured_output import _SIMPLIFIED_MODEL + + # Generate schema from the simplified model's JSON schema + schema = _SIMPLIFIED_MODEL.model_json_schema() + # Create a minimal example for reference + example = { + "name": "Example Repository", + "repositoryType": "software", + "repositoryTypeJustification": ["Contains source code"], + } - # Accumulate estimated tokens - if usage and "estimated_input_tokens" in usage: - self.estimated_input_tokens += usage.get("estimated_input_tokens", 0) - self.estimated_output_tokens += usage.get("estimated_output_tokens", 0) + structured_result = await generate_structured_output( + compiled_context=compiled_context, + schema=schema, + example=example, + ) + + structured_output = structured_result.get("data") + usage = structured_result.get("usage") + + if not structured_output: + logger.error("Structured output generation failed") + return + + # Accumulate usage from structured output + if usage: + self.total_input_tokens += usage.get("input_tokens", 0) + self.total_output_tokens += usage.get("output_tokens", 0) + if "estimated_input_tokens" in usage: + self.estimated_input_tokens += usage.get("estimated_input_tokens", 0) + self.estimated_output_tokens += usage.get("estimated_output_tokens", 0) + + # Log Stage 2 token usage + logger.info("=" * 80) + logger.info("STAGE 2 (Structured Output) Token Usage:") + logger.info( + f" Input tokens: {usage.get('input_tokens', 0):,} (official) | {usage.get('estimated_input_tokens', 0):,} (estimated)", + ) + logger.info( + f" Output tokens: {usage.get('output_tokens', 0):,} (official) | {usage.get('estimated_output_tokens', 0):,} (estimated)", + ) logger.info( - f"LLM analysis estimated: {usage.get('estimated_input_tokens', 0)} input, {usage.get('estimated_output_tokens', 0)} output tokens", + f" Total tokens: {usage.get('input_tokens', 0) + usage.get('output_tokens', 0):,}", ) + logger.info("=" * 80) - # Output Validation - if isinstance(llm_data, str): - llm_data = SoftwareSourceCode.model_validate_json(llm_data) - elif isinstance(llm_data, dict): - llm_data = SoftwareSourceCode.model_validate(llm_data) + # Stage 3: Classify repository type and discipline + logger.info("Stage 3: Classifying repository type and discipline...") + classification_result = await classify_repository_type_and_discipline( + compiled_context=compiled_context, + ) - if isinstance(llm_data, SoftwareSourceCode): - self.data = llm_data + classification = classification_result.get("data") + usage = classification_result.get("usage") - # TODO: Handle errors and logging + if not classification: + logger.error("Repository classification failed") + return + + # Accumulate usage from classification + if usage: + self.total_input_tokens += usage.get("input_tokens", 0) + self.total_output_tokens += usage.get("output_tokens", 0) + if "estimated_input_tokens" in usage: + self.estimated_input_tokens += usage.get("estimated_input_tokens", 0) + self.estimated_output_tokens += usage.get("estimated_output_tokens", 0) + + # Log Stage 3 token usage + logger.info("=" * 80) + logger.info("STAGE 3 (Repository Classifier) Token Usage:") + logger.info( + f" Input tokens: {usage.get('input_tokens', 0):,} (official) | {usage.get('estimated_input_tokens', 0):,} (estimated)", + ) + logger.info( + f" Output tokens: {usage.get('output_tokens', 0):,} (official) | {usage.get('estimated_output_tokens', 0):,} (estimated)", + ) + logger.info( + f" Total tokens: {usage.get('input_tokens', 0) + usage.get('output_tokens', 0):,}", + ) + logger.info("=" * 80) + + # Convert simplified output to SoftwareSourceCode + # First convert to dict + if hasattr(structured_output, "model_dump"): + simplified_dict = structured_output.model_dump() + else: + simplified_dict = structured_output + + # Override with classification results (Stage 3 takes precedence) + if hasattr(classification, "model_dump"): + classification_dict = classification.model_dump() + else: + classification_dict = classification + + # Merge classification into simplified_dict (overrides Stage 2 values) + simplified_dict["repositoryType"] = classification_dict.get("repositoryType") + simplified_dict["repositoryTypeJustification"] = classification_dict.get( + "repositoryTypeJustification", + [], + ) + simplified_dict["discipline"] = classification_dict.get("discipline", []) + simplified_dict["disciplineJustification"] = classification_dict.get( + "disciplineJustification", + [], + ) + + logger.info( + f"Repository classified as: {classification_dict.get('repositoryType')} with disciplines: {classification_dict.get('discipline', [])}", + ) + + # Stage 4: Identify related organizations + logger.info("Stage 4: Identifying related organizations...") + organization_result = await identify_related_organizations( + compiled_context=compiled_context, + ) + + organization_data = organization_result.get("data") + usage = organization_result.get("usage") + + if not organization_data: + logger.error("Organization identification failed") + return + + # Accumulate usage from organization identification + if usage: + self.total_input_tokens += usage.get("input_tokens", 0) + self.total_output_tokens += usage.get("output_tokens", 0) + if "estimated_input_tokens" in usage: + self.estimated_input_tokens += usage.get("estimated_input_tokens", 0) + self.estimated_output_tokens += usage.get("estimated_output_tokens", 0) + + # Log Stage 4 token usage + logger.info("=" * 80) + logger.info("STAGE 4 (Organization Identifier) Token Usage:") + logger.info( + f" Input tokens: {usage.get('input_tokens', 0):,} (official) | {usage.get('estimated_input_tokens', 0):,} (estimated)", + ) + logger.info( + f" Output tokens: {usage.get('output_tokens', 0):,} (official) | {usage.get('estimated_output_tokens', 0):,} (estimated)", + ) + logger.info( + f" Total tokens: {usage.get('input_tokens', 0) + usage.get('output_tokens', 0):,}", + ) + logger.info("=" * 80) + + # Convert organization data to dict + if hasattr(organization_data, "model_dump"): + organization_dict = organization_data.model_dump() + else: + organization_dict = organization_data + + # Merge organization data into simplified_dict (Stage 4 provides organizations) + simplified_dict["relatedToOrganizations"] = organization_dict.get( + "relatedToOrganizations", + [], + ) + simplified_dict["relatedToOrganizationJustification"] = organization_dict.get( + "relatedToOrganizationJustification", + [], + ) + + logger.info( + f"Identified {len(organization_dict.get('relatedToOrganizations', []))} related organizations", + ) + + # Get union_metadata for reconciliation + union_metadata = structured_result.get("union_metadata", {}) + + # Convert simplified dict to full SoftwareSourceCode format + full_dict = self._convert_simplified_to_full( + simplified_dict, + union_metadata, + git_authors=git_authors, + ) + + # Note: EPFL assessment now runs AFTER all enrichments complete + # (see run_epfl_final_assessment method called at end of run_analysis) + + # Validate and create SoftwareSourceCode + try: + self.data = SoftwareSourceCode.model_validate(full_dict) + logger.info("Atomic LLM pipeline completed successfully") + except Exception as e: + logger.error(f"Failed to validate SoftwareSourceCode: {e}", exc_info=True) + + def _extract_gimie_fields(self) -> dict: + """ + Extract fields from GIMIE JSON-LD data that can be automatically populated. + + Returns: + Dictionary with GIMIE-extracted fields + """ + gimie_dict = {} + + if not self.gimie: + logger.debug("No GIMIE data available for field extraction") + return gimie_dict + + # GIMIE can be in different formats: + # 1. A list directly: [{"@id": "...", "@type": [...]}, ...] + # 2. A dict with @graph: {"@graph": [{"@id": "...", ...}, ...]} + # 3. A dict that is the graph itself + graph = None + if isinstance(self.gimie, list): + # Format 1: Direct list + graph = self.gimie + logger.debug(f"GIMIE data is a list with {len(graph)} entities") + elif isinstance(self.gimie, dict): + # Format 2: Dict with @graph + if "@graph" in self.gimie: + graph = self.gimie.get("@graph", []) + logger.debug(f"GIMIE data has @graph with {len(graph)} entities") + else: + # Format 3: Single entity dict (treat as list with one item) + graph = [self.gimie] + logger.debug("GIMIE data is a single entity dict") + else: + logger.warning(f"Unexpected GIMIE data type: {type(self.gimie)}") + return gimie_dict + + if not graph: + logger.debug("GIMIE graph is empty") + return gimie_dict + for entity in graph: + if not isinstance(entity, dict): + continue + + entity_types = entity.get("@type", []) + if not isinstance(entity_types, list): + entity_types = [entity_types] + + # Check if this is a SoftwareSourceCode entity + if "http://schema.org/SoftwareSourceCode" not in entity_types: + logger.debug(f"Skipping entity type: {entity_types}") + continue + + logger.debug("Found SoftwareSourceCode entity, extracting fields...") + + # Extract @id (repository URL) - this becomes the id field + entity_id = entity.get("@id") + if entity_id: + gimie_dict["id"] = entity_id + logger.debug(f"Extracted id from GIMIE: {entity_id}") + + # Helper to get value from JSON-LD + def get_ld_value(key: str): + value = entity.get(key) + if value is None: + return None + if isinstance(value, dict): + return value.get("@value") or value.get("@id") + if isinstance(value, list): + return [ + v.get("@value") if isinstance(v, dict) else v for v in value + ] + return value + + # Extract name (can be string or array with @value objects) + # Check both prefixed and full URI formats + name_val = entity.get("http://schema.org/name") or entity.get("schema:name") + if name_val: + if isinstance(name_val, list): + # Get first name from array + if name_val and isinstance(name_val[0], dict): + name_str = name_val[0].get("@value") + if name_str: + gimie_dict["name"] = name_str + elif name_val: + gimie_dict["name"] = name_val[0] + elif isinstance(name_val, dict): + name_str = name_val.get("@value") + if name_str: + gimie_dict["name"] = name_str + else: + gimie_dict["name"] = name_val + + # Extract codeRepository + # Check both prefixed and full URI formats + code_repo = entity.get("http://schema.org/codeRepository") or entity.get( + "schema:codeRepository", + ) + if code_repo: + if isinstance(code_repo, list): + gimie_dict["codeRepository"] = [ + r.get("@id") if isinstance(r, dict) else r for r in code_repo + ] + elif isinstance(code_repo, dict): + gimie_dict["codeRepository"] = [code_repo.get("@id", code_repo)] + else: + gimie_dict["codeRepository"] = [code_repo] + + # Extract license (can be array with @id objects) + # Check both prefixed and full URI formats + license_val = entity.get("http://schema.org/license") or entity.get( + "schema:license", + ) + if license_val: + if isinstance(license_val, list): + # Get first license from array + if license_val and isinstance(license_val[0], dict): + license_str = license_val[0].get("@id") + if license_str: + gimie_dict["license"] = license_str + elif isinstance(license_val, dict): + license_str = license_val.get("@id") + if license_str: + gimie_dict["license"] = license_str + else: + gimie_dict["license"] = license_val + + # Extract dateCreated (can be array with @value objects) + # Check both prefixed and full URI formats + date_created = entity.get("http://schema.org/dateCreated") or entity.get( + "schema:dateCreated", + ) + if date_created: + if isinstance(date_created, list): + # Get first date from array + if date_created and isinstance(date_created[0], dict): + date_str = date_created[0].get("@value") + if date_str: + gimie_dict["dateCreated"] = date_str + elif isinstance(date_created, dict): + date_str = date_created.get("@value") + if date_str: + gimie_dict["dateCreated"] = date_str + else: + gimie_dict["dateCreated"] = date_created + + # Extract datePublished (can be array with @value objects) + # Check both prefixed and full URI formats + date_pub = entity.get("http://schema.org/datePublished") or entity.get( + "schema:datePublished", + ) + if date_pub: + if isinstance(date_pub, list): + # Get first date from array + if date_pub and isinstance(date_pub[0], dict): + date_str = date_pub[0].get("@value") + if date_str: + gimie_dict["datePublished"] = date_str + elif isinstance(date_pub, dict): + date_str = date_pub.get("@value") + if date_str: + gimie_dict["datePublished"] = date_str + else: + gimie_dict["datePublished"] = date_pub + + # Extract dateModified (can be array with @value objects) + # Check both prefixed and full URI formats + date_modified = entity.get("http://schema.org/dateModified") or entity.get( + "schema:dateModified", + ) + if date_modified: + if isinstance(date_modified, list): + # Get first date from array + if date_modified and isinstance(date_modified[0], dict): + date_str = date_modified[0].get("@value") + if date_str: + gimie_dict["dateModified"] = date_str + elif isinstance(date_modified, dict): + date_str = date_modified.get("@value") + if date_str: + gimie_dict["dateModified"] = date_str + else: + gimie_dict["dateModified"] = date_modified + + # Extract url + # Check both prefixed and full URI formats + url_val = entity.get("http://schema.org/url") or entity.get("schema:url") + if url_val: + url_str = url_val.get("@id") if isinstance(url_val, dict) else url_val + if url_str: + gimie_dict["url"] = url_str + + # Extract programmingLanguage + # Check both prefixed and full URI formats + prog_lang = entity.get( + "http://schema.org/programmingLanguage", + ) or entity.get( + "schema:programmingLanguage", + ) + if prog_lang: + if isinstance(prog_lang, list): + gimie_dict["programmingLanguage"] = [ + lang.get("@value") if isinstance(lang, dict) else lang + for lang in prog_lang + ] + else: + lang_val = ( + prog_lang.get("@value") + if isinstance(prog_lang, dict) + else prog_lang + ) + if lang_val: + gimie_dict["programmingLanguage"] = [lang_val] + + # Extract keywords (can be array with @value objects) + # Check both prefixed and full URI formats + keywords = entity.get("http://schema.org/keywords") or entity.get( + "schema:keywords", + ) + if keywords: + if isinstance(keywords, list): + # Extract @value from each keyword object + keyword_list = [] + for kw in keywords: + if isinstance(kw, dict): + kw_val = kw.get("@value") + if kw_val: + keyword_list.append(kw_val) + else: + keyword_list.append(kw) + if keyword_list: + gimie_dict["keywords"] = keyword_list + else: + kw_val = ( + keywords.get("@value") + if isinstance(keywords, dict) + else keywords + ) + if kw_val: + gimie_dict["keywords"] = ( + [kw_val] if isinstance(kw_val, str) else kw_val + ) + + # Extract readme + # Check both prefixed and full URI formats + readme_val = entity.get("https://w3id.org/okn/o/sd#readme") or entity.get( + "sd:readme", + ) + if readme_val: + readme_str = ( + readme_val.get("@id") + if isinstance(readme_val, dict) + else readme_val + ) + if readme_str: + gimie_dict["readme"] = readme_str + + # Extract citation + # Check both prefixed and full URI formats + citation = entity.get("http://schema.org/citation") or entity.get( + "schema:citation", + ) + if citation: + if isinstance(citation, list): + gimie_dict["citation"] = [ + cit.get("@id") if isinstance(cit, dict) else cit + for cit in citation + ] + else: + cit_val = ( + citation.get("@id") if isinstance(citation, dict) else citation + ) + if cit_val: + gimie_dict["citation"] = [cit_val] + + # Only process the first SoftwareSourceCode entity + break + + return gimie_dict + + def _extract_gimie_authors_and_organizations(self) -> dict: + """ + Extract authors (Person) and organizations from GIMIE JSON-LD data. + Resolves affiliations and maintains @id references. + + Returns: + Dictionary with 'authors' and 'organizations' lists in structured format + """ + result = { + "authors": [], + "organizations": [], + } + + if not self.gimie: + return result + + # Get graph (handle different formats) + graph = None + if isinstance(self.gimie, list): + graph = self.gimie + elif isinstance(self.gimie, dict): + if "@graph" in self.gimie: + graph = self.gimie.get("@graph", []) + else: + graph = [self.gimie] + + if not graph: + return result + + # Build entity lookup by @id for affiliation resolution + entity_lookup = {} + for entity in graph: + if isinstance(entity, dict) and "@id" in entity: + entity_lookup[entity["@id"]] = entity + + # Helper to extract value from JSON-LD field + def extract_value(field_value): + """Extract actual value from JSON-LD field (handles @value, @id, arrays)""" + if field_value is None: + return None + if isinstance(field_value, list): + if not field_value: + return None + # Get first value + first = field_value[0] + if isinstance(first, dict): + return first.get("@value") or first.get("@id") + return first + if isinstance(field_value, dict): + return field_value.get("@value") or field_value.get("@id") + return field_value + + # Helper to extract list of values + def extract_list(field_value): + """Extract list of values from JSON-LD field""" + if field_value is None: + return [] + if isinstance(field_value, list): + result_list = [] + for item in field_value: + if isinstance(item, dict): + value = item.get("@value") or item.get("@id") + if value: + result_list.append(value) + else: + result_list.append(item) + return result_list + # Single value + if isinstance(field_value, dict): + value = field_value.get("@value") or field_value.get("@id") + return [value] if value else [] + return [field_value] + + # Extract organizations first (needed for affiliation resolution) + organizations_by_id = {} + for entity in graph: + if not isinstance(entity, dict): + continue + + entity_types = entity.get("@type", []) + if not isinstance(entity_types, list): + entity_types = [entity_types] + + if "http://schema.org/Organization" not in entity_types: + continue + + entity_id = entity.get("@id") + if not entity_id: + continue + + org_data = { + "id": entity_id, + "legalName": extract_value( + entity.get("http://schema.org/legalName") + or entity.get("schema:legalName"), + ), + "name": extract_value( + entity.get("http://schema.org/name") or entity.get("schema:name"), + ), + "description": extract_value( + entity.get("http://schema.org/description") + or entity.get("schema:description"), + ), + } + + # Extract logo if available + logo = extract_value( + entity.get("http://schema.org/logo") or entity.get("schema:logo"), + ) + if logo: + org_data["logo"] = logo + + organizations_by_id[entity_id] = org_data + result["organizations"].append(org_data) + + # Extract authors (Person entities) + for entity in graph: + if not isinstance(entity, dict): + continue + + entity_types = entity.get("@type", []) + if not isinstance(entity_types, list): + entity_types = [entity_types] + + if "http://schema.org/Person" not in entity_types: + continue + + entity_id = entity.get("@id") + if not entity_id: + continue + + # Extract basic person fields + person_data = { + "id": entity_id, + "name": extract_value( + entity.get("http://schema.org/name") or entity.get("schema:name"), + ), + } + + # Extract GitHub ID from GitHub URL if present in entity_id + if entity_id and "github.com/" in entity_id: + # Extract username from URL like "https://github.com/username" + try: + github_username = entity_id.rstrip("/").split("/")[-1] + if github_username and github_username != "github.com": + person_data["githubId"] = github_username + except Exception as e: + logger.debug(f"Could not extract GitHub ID from {entity_id}: {e}") + + # Extract identifier (GitHub username, etc.) + identifier = extract_value( + entity.get("http://schema.org/identifier") + or entity.get("schema:identifier"), + ) + if identifier: + person_data["identifier"] = identifier + + # Extract ORCID + orcid = extract_value( + entity.get("http://w3id.org/nfdi4ing/metadata4ing#orcidId") + or entity.get("md4i:orcidId"), + ) + if orcid: + normalized_orcid = self._normalize_orcid(orcid) + if normalized_orcid: + person_data["orcid"] = normalized_orcid + else: + logger.warning( + f"Dropping invalid ORCID extracted from GIMIE for {person_data.get('name')}: {orcid}", + ) + + # Extract affiliations and resolve them + affiliations_raw = entity.get( + "http://schema.org/affiliation", + ) or entity.get( + "schema:affiliation", + ) + affiliations = [] + if affiliations_raw: + affiliation_list = extract_list(affiliations_raw) + for aff in affiliation_list: + if isinstance(aff, str): + org_name = None + org_id = None + + # Could be an @id reference or a string value + if aff.startswith("http://") or aff.startswith("https://"): + # It's an @id reference - resolve to organization + if aff in organizations_by_id: + org_data = organizations_by_id[aff] + # Extract name from organization data + org_name = ( + org_data.get("legalName") + or org_data.get("name") + or aff + ) + org_id = aff # Store the URL as ID + else: + # Reference not found, use as string + org_name = aff + org_id = aff + else: + # String value (organization name) + org_name = aff + + affiliations.append( + Affiliation( + name=org_name, + organizationId=org_id, + source="gimie", + ), + ) + + if affiliations: + person_data["affiliations"] = affiliations + logger.debug( + f"GIMIE author {person_data.get('name')} has {len(affiliations)} affiliations", + ) + + result["authors"].append(person_data) + + logger.info( + f"Extracted {len(result['authors'])} authors and {len(result['organizations'])} organizations from GIMIE", + ) + # Log total affiliations extracted + total_affs = sum(len(a.get("affiliations", [])) for a in result["authors"]) + logger.info(f"Total affiliations extracted from GIMIE: {total_affs}") + return result + + def _deduplicate_authors(self): + """ + Deduplicate authors with the same name, merging their fields. + + This merges Person objects that have the same name but different IDs, + combining their affiliations, emails, and other metadata without duplicates. + """ + if not self.data or not hasattr(self.data, "author") or not self.data.author: + return + + from collections import defaultdict + + # Group authors by name (case-insensitive, normalize hyphens/dashes) + authors_by_name = defaultdict(list) + for author in self.data.author: + if hasattr(author, "name") and author.name: + # Normalize name for grouping + # Replace various dash/hyphen characters with standard hyphen + normalized_name = author.name.strip().lower() + # Normalize various unicode hyphens/dashes to regular hyphen + normalized_name = normalized_name.replace( + "‑", + "-", + ) # non-breaking hyphen + normalized_name = normalized_name.replace("–", "-") # en dash + normalized_name = normalized_name.replace("—", "-") # em dash + authors_by_name[normalized_name].append(author) + + # Merge duplicates + merged_authors = [] + for normalized_name, author_list in authors_by_name.items(): + if len(author_list) == 1: + # No duplicates + merged_authors.append(author_list[0]) + else: + # Multiple authors with same name - merge them + logger.info( + f"Merging {len(author_list)} duplicate authors: {author_list[0].name}", + ) + merged = self._merge_person_objects(author_list) + merged_authors.append(merged) + + self.data.author = merged_authors + # Log affiliation counts after deduplication + total_affs_after_dedup = sum( + len(a.affiliations) if hasattr(a, "affiliations") else 0 + for a in self.data.author + ) + logger.info( + f"Author deduplication complete: {len(self.data.author)} unique authors with {total_affs_after_dedup} total affiliations", + ) + + def _merge_person_objects(self, persons: list): + """ + Merge multiple Person objects with the same name into one. + + Args: + persons: List of Person objects to merge + + Returns: + Merged Person object + """ + from ..data_models.models import Person + + if len(persons) == 1: + return persons[0] + + # Prioritize the person with the most information (prefer orcid > github > gimie) + # Sort by: has ORCID, has affiliations, source priority + source_priority = { + "orcid": 3, + "agent_user_enrichment": 2, + "github_profile": 1, + "gimie": 0, + } + + def person_score(p): + score = 0 + if hasattr(p, "orcid") and p.orcid: + score += 100 + if hasattr(p, "affiliations") and p.affiliations: + score += 10 * len(p.affiliations) + if hasattr(p, "linkedEntities") and p.linkedEntities: + score += 5 * len(p.linkedEntities) + if hasattr(p, "source") and p.source: + # Check if source contains any priority keywords + for source_key, priority in source_priority.items(): + if source_key in str(p.source).lower(): + score += priority + break + return score + + # Use the person with the highest score as base + base = max(persons, key=person_score) + + # Collect all unique values across all persons + all_ids = [] + all_emails = [] + all_github_ids = [] + all_orcids = [] + all_affiliations = [] + all_affiliation_history = [] + all_linked_entities = [] + all_sources = [] + + for person in persons: + # IDs + if person.id: + all_ids.append(person.id) + + # Emails + if person.emails: + all_emails.extend(person.emails) + + # GitHub IDs + if hasattr(person, "githubId") and person.githubId: + all_github_ids.append(person.githubId) + + # ORCIDs - prefer full URLs + if person.orcid: + normalized_orcid = self._normalize_orcid(person.orcid) + if normalized_orcid: + all_orcids.append(normalized_orcid) + else: + logger.warning( + f"Dropping invalid ORCID while merging duplicate author {person.name}: {person.orcid}", + ) + + # Affiliations + if person.affiliations: + all_affiliations.extend(person.affiliations) + + # Affiliation history + if person.affiliationHistory: + all_affiliation_history.extend(person.affiliationHistory) + + # Linked entities + if hasattr(person, "linkedEntities") and person.linkedEntities: + all_linked_entities.extend(person.linkedEntities) + + # Sources + if person.source: + all_sources.append(person.source) + + # Deduplicate and merge + # ID: Priority - GitHub URL > ORCID URL > internal ID (gitAuthor) > hash name + # Note: If we have both GitHub and ORCID, use GitHub URL for id field + merged_id = None + github_urls = [id for id in all_ids if "github.com" in id] + orcid_urls = [id for id in all_ids if "orcid.org" in id] + git_author_hashes = [ + id + for id in all_ids + if len(id) == 64 and all(c in "0123456789abcdef" for c in id) + ] # SHA-256 hash + other_ids = [ + id + for id in all_ids + if id not in github_urls + orcid_urls + git_author_hashes + ] + + # Prefer GitHub URL as the primary ID + if github_urls: + merged_id = github_urls[0] + elif orcid_urls: + merged_id = orcid_urls[0] + elif git_author_hashes: + merged_id = git_author_hashes[0] + elif other_ids: + merged_id = other_ids[0] + elif all_ids: + merged_id = all_ids[0] + + # GitHub ID: Deduplicate and extract from URLs if needed + merged_github_id = None + if all_github_ids: + # Prefer non-URL format + non_url_github = [g for g in all_github_ids if not g.startswith("http")] + if non_url_github: + merged_github_id = non_url_github[0] + else: + merged_github_id = all_github_ids[0] + + # If we have GitHub URLs but no explicit githubId, extract from URL + if not merged_github_id and github_urls: + # Extract from GitHub URL (e.g., https://github.com/username) + github_url = github_urls[0] + if github_url.startswith("https://github.com/"): + username = github_url.replace("https://github.com/", "").split("/")[0] + if username: # Ensure we extracted a valid username + merged_github_id = username + + # Emails: Deduplicate (case-insensitive) + merged_emails = list( + {email.lower(): email for email in all_emails if email}.values(), + ) + + # ORCID: Keep canonical ID format (xxxx-xxxx-xxxx-xxxx) + merged_orcid = all_orcids[0] if all_orcids else None + + # Affiliations: Deduplicate by name (case-insensitive) + # Keep the one with the most information (non-empty organizationId preferred) + affiliation_dict = {} + for aff in all_affiliations: + aff_name_lower = aff.name.lower() if aff.name else "" + if not aff_name_lower: + continue + + # Check if this affiliation has a valid (non-empty) organizationId + has_org_id = bool(aff.organizationId and aff.organizationId.strip()) + + if aff_name_lower not in affiliation_dict: + # First occurrence of this affiliation + affiliation_dict[aff_name_lower] = aff + else: + # Duplicate - keep the one with more information + existing = affiliation_dict[aff_name_lower] + existing_has_org_id = bool( + existing.organizationId and existing.organizationId.strip(), + ) + + # Prefer the one with a non-empty organizationId + if has_org_id and not existing_has_org_id: + affiliation_dict[aff_name_lower] = aff + # If both have organizationId or neither has it, keep the first one + + merged_affiliations = list(affiliation_dict.values()) + + # Affiliation history: Deduplicate strings + merged_affiliation_history = list(set(all_affiliation_history)) + + # Linked entities: Deduplicate based on UUID or entity content + # Use a dictionary to track unique entities by UUID (or full entity if no UUID) + unique_entities = {} + for linked_entity in all_linked_entities: + if not linked_entity: + continue + + # Try to get a unique key for this linked entity + entity_key = None + + # Check if entity has a UUID (most reliable) + if hasattr(linked_entity, "entity") and linked_entity.entity: + entity_obj = linked_entity.entity + if hasattr(entity_obj, "uuid") and entity_obj.uuid: + entity_key = f"{linked_entity.catalogType}:{linked_entity.entityType}:{entity_obj.uuid}" + elif hasattr(entity_obj, "id"): + entity_key = f"{linked_entity.catalogType}:{linked_entity.entityType}:{entity_obj.id}" + + # Fallback: Use catalog type + entity type + justification + if not entity_key: + entity_key = f"{linked_entity.catalogType}:{linked_entity.entityType}:{linked_entity.justification}" + + # Add to dict (overwrites duplicates) + if entity_key not in unique_entities: + unique_entities[entity_key] = linked_entity + + merged_linked_entities = list(unique_entities.values()) + + # Sources: Combine unique sources + merged_sources = ", ".join(sorted(set(all_sources))) + + # Create merged Person + merged = Person( + type="Person", + id=merged_id or base.id, + name=base.name, # Use original name (not normalized) + emails=merged_emails, + githubId=merged_github_id, + orcid=merged_orcid, + affiliations=merged_affiliations, + affiliationHistory=merged_affiliation_history, + source=merged_sources, + ) + + # Add linked entities if the field exists + if hasattr(merged, "linkedEntities"): + merged.linkedEntities = merged_linked_entities + + # Log detailed affiliation info for debugging + input_affs = sum( + len(p.affiliations) if hasattr(p, "affiliations") else 0 for p in persons + ) + logger.debug( + f"Merged {len(persons)} instances of {base.name}: " + f"input {input_affs} affiliations → output {len(merged_affiliations)} unique affiliations, " + f"{len(merged_linked_entities)} linked entities", + ) + + return merged + + def _reconcile_entity_union(self, rel_dict: dict) -> dict: + """ + Reconcile entity Union fields that were split during simplification. + + The entity field Union[InfosciencePublication, InfoscienceAuthor, InfoscienceOrgUnit] + gets split into: + - entityInfosciencePublication + - entityInfoscienceAuthor + - entityInfoscienceOrgUnit + + This method merges them back into the single 'entity' field based on entityType. + + Args: + rel_dict: Relation dict with split entity fields + + Returns: + Relation dict with unified 'entity' field + """ + # Get entityType to determine which union variant to use + entity_type = rel_dict.get("entityType", "").lower() + + # Extract all three possible entity fields + entity_pub = rel_dict.pop("entityInfosciencePublication", None) + entity_author = rel_dict.pop("entityInfoscienceAuthor", None) + entity_lab = rel_dict.pop("entityInfoscienceOrgUnit", None) + + # Select the correct entity based on entityType + selected_entity = None + if entity_type == "publication" and entity_pub is not None: + selected_entity = entity_pub + elif entity_type == "person" and entity_author is not None: + selected_entity = entity_author + elif entity_type == "orgunit" and entity_lab is not None: + selected_entity = entity_lab + else: + # Fallback: use whichever one is not None (if entityType doesn't match) + if entity_pub is not None: + selected_entity = entity_pub + elif entity_author is not None: + selected_entity = entity_author + elif entity_lab is not None: + selected_entity = entity_lab + + # Clean up None values in list fields (convert to empty list) + if selected_entity and isinstance(selected_entity, dict): + # Handle subjects field (should be list, not None) + if "subjects" in selected_entity and selected_entity["subjects"] is None: + selected_entity["subjects"] = [] + # Handle other list fields that might be None + for key, value in selected_entity.items(): + if value is None and key in ["authors", "keywords", "subjects"]: + selected_entity[key] = [] + + # Set the unified entity field + if selected_entity is not None: + rel_dict["entity"] = selected_entity + # If all None, entity will remain None (which is allowed) + + return rel_dict + + def _convert_simplified_to_full( + self, + simplified_dict: dict, + union_metadata: Optional[dict] = None, + git_authors: Optional[list] = None, + ) -> dict: + """ + Convert simplified output dict to full SoftwareSourceCode format. + Merges model output with GIMIE/git extracted data. + + Args: + simplified_dict: Simplified output from structured output agent + union_metadata: Metadata about Union fields that were split (for reconciliation) + git_authors: Optional list of GitAuthor objects extracted from the repository + + Returns: + Dictionary in full SoftwareSourceCode format with merged GIMIE/git data + """ + from datetime import date + + from pydantic import BaseModel, HttpUrl + + from ..data_models.models import RepositoryType + from ..data_models.repository import GitAuthor + + if union_metadata is None: + union_metadata = {} + + # Start with model output + full_dict = simplified_dict.copy() + + # Extract GIMIE fields + gimie_dict = self._extract_gimie_fields() + logger.debug( + f"Extracted {len(gimie_dict)} fields from GIMIE: {list(gimie_dict.keys())}", + ) + + # Extract GIMIE authors and organizations for merging + gimie_authors_orgs = self._extract_gimie_authors_and_organizations() + + # id - prioritize GIMIE @id, then repository full_path, then model + if "id" in gimie_dict and gimie_dict.get("id"): + full_dict["id"] = gimie_dict["id"] + logger.info(f"Using id from GIMIE: {gimie_dict['id']}") + elif self.full_path: + full_dict["id"] = self.full_path + logger.info(f"Using repository full_path as id: {self.full_path}") + elif "id" in simplified_dict and simplified_dict.get("id"): + full_dict["id"] = simplified_dict["id"] + logger.debug(f"Using id from model: {simplified_dict['id']}") + else: + # Fallback: use empty string (default) + full_dict["id"] = "" + + # Helper function to clean None values for list fields in any model dict + def clean_model_dict(model_dict: dict, model_type: type) -> dict: + """Convert None to empty lists for fields with default_factory=list""" + if not isinstance(model_dict, dict): + return model_dict + + cleaned_dict = model_dict.copy() + + # Normalize ORCID for Person payloads to avoid hard validation failures. + if getattr(model_type, "__name__", "") == "Person": + cleaned_dict = self._sanitize_person_payload( + cleaned_dict, + "union reconciliation", + ) + + # Check if model_type has model_fields + if hasattr(model_type, "model_fields"): + for field_name, field_info in model_type.model_fields.items(): + if field_name in cleaned_dict and cleaned_dict[field_name] is None: + # Check if field has default_factory and it's callable + if ( + hasattr(field_info, "default_factory") + and field_info.default_factory is not ... + and field_info.default_factory is not None + and callable(field_info.default_factory) + ): + # Convert None to empty value from default_factory + cleaned_dict[field_name] = field_info.default_factory() + return cleaned_dict + + # Handle Union fields first + for original_field, union_info_list in union_metadata.items(): + reconciled_values = [] + is_list = False # Default to not being a list + for union_info in union_info_list: + is_list = union_info.get("is_list", False) + for new_field_name, field_data in union_info["fields"].items(): + if ( + new_field_name in simplified_dict + and simplified_dict[new_field_name] is not None + ): + values = simplified_dict[new_field_name] + + # Ensure values is a list for consistent processing + if not isinstance(values, list): + values = [values] + + for value in values: + target_type = field_data["type"] + if isinstance(target_type, type) and issubclass( + target_type, + BaseModel, + ): + if isinstance(value, dict): + # Clean None values before instantiation + value = clean_model_dict(value, target_type) + try: + reconciled_values.append(target_type(**value)) + except Exception as e: + logger.warning( + f"Failed to instantiate {target_type.__name__} during union reconciliation: {e}", + ) + # Keep raw dict so a single bad record does not abort the full request. + reconciled_values.append(value) + else: + reconciled_values.append( + value, + ) # Already a model instance + else: + reconciled_values.append(value) + + # Remove the split field from the dictionary + if new_field_name in full_dict: + del full_dict[new_field_name] + + if reconciled_values: + if is_list: + full_dict[original_field] = reconciled_values + else: + full_dict[original_field] = ( + reconciled_values[0] if reconciled_values else None + ) + else: + full_dict[original_field] = [] if is_list else None + + # Merge GIMIE authors with model authors (after Union reconciliation) + # Convert GIMIE authors to Person objects and merge with model output + if gimie_authors_orgs.get("authors"): + from ..data_models.models import Affiliation, Person + + gimie_authors = [] + for gimie_author in gimie_authors_orgs["authors"]: + # Convert GIMIE author dict to Person object + person_id = gimie_author.get("id", "") + person_data = { + "type": "Person", + "id": person_id, + "name": gimie_author.get("name", ""), + "source": "gimie", + } + + # Extract GitHub ID from GitHub URL if present + if person_id and "github.com/" in person_id: + # Extract username from URL like "https://github.com/username" + try: + github_username = person_id.rstrip("/").split("/")[-1] + if github_username and github_username != "github.com": + person_data["githubId"] = github_username + except Exception as e: + logger.debug( + f"Could not extract GitHub ID from {person_id}: {e}", + ) + + # Add ORCID if available + if gimie_author.get("orcid"): + normalized_orcid = self._normalize_orcid(gimie_author["orcid"]) + if normalized_orcid: + person_data["orcid"] = normalized_orcid + else: + logger.warning( + f"Dropping invalid ORCID from GIMIE author {gimie_author.get('name')}: {gimie_author.get('orcid')}", + ) + + # Add identifier if available + if gimie_author.get("identifier"): + # Store identifier in a way that can be used later + # For now, we'll add it to affiliations or keep it separate + pass + + # Convert affiliations - handle Affiliation objects, organization objects, and strings + affiliations = [] + if gimie_author.get("affiliations"): + for aff in gimie_author["affiliations"]: + # If it's already an Affiliation object, keep it + if isinstance(aff, Affiliation): + affiliations.append(aff) + elif isinstance(aff, dict): + # Could be Affiliation dict or Organization dict + if "name" in aff and "source" in aff: + # Affiliation object - validate name is a string + aff_name = aff.get("name") + if isinstance(aff_name, str): + affiliations.append(aff) + else: + # Name is not a string, try to extract it + logger.warning( + f"Affiliation name is not a string: {type(aff_name)}", + ) + if isinstance(aff_name, dict): + aff_name = aff_name.get( + "legalName", + ) or aff_name.get("name") + if isinstance(aff_name, str): + affiliations.append( + Affiliation( + name=aff_name, + organizationId=aff.get( + "organizationId", + ), + source=aff.get("source", "gimie"), + ), + ) + elif "legalName" in aff or "name" in aff: + # Organization object - convert to Affiliation + org_name = aff.get("legalName") or aff.get("name") + # Ensure org_name is a string + if isinstance(org_name, dict): + org_name = org_name.get( + "legalName", + ) or org_name.get("name") + if org_name and isinstance(org_name, str): + affiliations.append( + Affiliation( + name=org_name, + organizationId=aff.get("id"), + source="gimie", + ), + ) + elif isinstance(aff, str): + # String affiliation - convert to Affiliation + affiliations.append( + Affiliation( + name=aff, + organizationId=None, + source="gimie", + ), + ) + + if affiliations: + person_data["affiliations"] = affiliations + + try: + person_data = self._sanitize_person_payload( + person_data, + "GIMIE author conversion", + ) + new_person = Person(**person_data) + gimie_authors.append(new_person) + logger.debug( + f"Created GIMIE Person {new_person.name} with {len(new_person.affiliations)} affiliations", + ) + except Exception as e: + logger.warning( + f"Failed to create Person from GIMIE author {gimie_author.get('name')}: {e}", + ) + + # Get existing authors from model output + existing_authors = full_dict.get("author", []) + if not isinstance(existing_authors, list): + existing_authors = [] + + # Convert existing authors to Person objects if they're dicts + existing_person_objects = [] + for author in existing_authors: + if isinstance(author, dict): + try: + # Clean None values before creating Person + author = clean_model_dict(author, Person) + author = self._sanitize_person_payload( + author, + "existing author conversion", + ) + existing_person_objects.append(Person(**author)) + except Exception as e: + logger.warning(f"Failed to convert author dict to Person: {e}") + # Keep as dict if conversion fails + existing_person_objects.append(author) + elif isinstance(author, Person): + existing_person_objects.append(author) + else: + existing_person_objects.append(author) + + # Build lookup of existing authors by name (normalized), id, and ORCID + existing_by_name = {} + existing_by_id = {} + existing_by_orcid = {} + for idx, author in enumerate(existing_person_objects): + if isinstance(author, Person): + name = author.name.lower() if author.name else None + author_id = author.id if author.id else None + orcid = self._normalize_orcid(author.orcid) if author.orcid else None + + if name: + existing_by_name[name] = idx + if author_id: + existing_by_id[author_id] = idx + if orcid: + existing_by_orcid[orcid] = idx + elif isinstance(author, dict): + name = ( + author.get("name", "").lower() if author.get("name") else None + ) + author_id = author.get("id") + orcid = self._normalize_orcid(author.get("orcid")) + + if name: + existing_by_name[name] = idx + if author_id: + existing_by_id[author_id] = idx + if orcid: + existing_by_orcid[orcid] = idx + + # Merge: update existing authors with GIMIE data, add new ones + merged_authors = list(existing_person_objects) + gimie_processed = set() + + for gimie_author in gimie_authors: + author_name = gimie_author.name.lower() if gimie_author.name else None + author_id = gimie_author.id if gimie_author.id else None + author_orcid = ( + self._normalize_orcid(gimie_author.orcid) + if gimie_author.orcid + else None + ) + + # Try to find matching existing author by ORCID (most reliable), then ID, then name + matched_idx = None + if author_orcid and author_orcid in existing_by_orcid: + matched_idx = existing_by_orcid[author_orcid] + elif author_id and author_id in existing_by_id: + matched_idx = existing_by_id[author_id] + elif author_name and author_name in existing_by_name: + matched_idx = existing_by_name[author_name] + + if matched_idx is not None: + # Update existing author with GIMIE data (especially ID) + existing_author = merged_authors[matched_idx] + if isinstance(existing_author, Person): + # Create updated Person object (Pydantic V2 is immutable) + updated_data = existing_author.model_dump() + updated = False + + # Update ID if missing + if not updated_data.get("id") and gimie_author.id: + updated_data["id"] = gimie_author.id + updated = True + logger.info( + f"Updated author {existing_author.name} with GIMIE ID: {gimie_author.id}", + ) + + # Update GitHub ID if missing and GIMIE author has one + if ( + not updated_data.get("githubId") + and hasattr(gimie_author, "githubId") + and gimie_author.githubId + ): + updated_data["githubId"] = gimie_author.githubId + updated = True + + # Update ORCID if missing + if not updated_data.get("orcid") and gimie_author.orcid: + normalized_orcid = self._normalize_orcid(gimie_author.orcid) + if normalized_orcid: + updated_data["orcid"] = normalized_orcid + updated = True + + # Merge affiliations + if gimie_author.affiliations: + existing_affs = updated_data.get("affiliations", []) + existing_names = { + aff.name.lower(): aff + for aff in existing_affs + if isinstance(aff, Affiliation) + } + added_count = 0 + for aff in gimie_author.affiliations: + if ( + isinstance(aff, Affiliation) + and aff.name.lower() not in existing_names + ): + updated_data.setdefault("affiliations", []).append( + aff, + ) + updated = True + added_count += 1 + if added_count > 0: + logger.debug( + f"Added {added_count} GIMIE affiliations to {existing_author.name}, total now: {len(updated_data.get('affiliations', []))}", + ) + + if updated: + try: + updated_data = self._sanitize_person_payload( + updated_data, + "merged author update", + ) + merged_authors[matched_idx] = Person(**updated_data) + except Exception as e: + logger.warning(f"Failed to update Person object: {e}") + elif isinstance(existing_author, dict): + # Update dict + updated = False + if not existing_author.get("id") and gimie_author.id: + existing_author["id"] = gimie_author.id + updated = True + logger.info( + f"Updated author {existing_author.get('name')} with GIMIE ID: {gimie_author.id}", + ) + if ( + not existing_author.get("githubId") + and hasattr(gimie_author, "githubId") + and gimie_author.githubId + ): + existing_author["githubId"] = gimie_author.githubId + updated = True + if not existing_author.get("orcid") and gimie_author.orcid: + normalized_orcid = self._normalize_orcid(gimie_author.orcid) + if normalized_orcid: + existing_author["orcid"] = normalized_orcid + updated = True + # Merge affiliations + if gimie_author.affiliations: + existing_affs = existing_author.get("affiliations", []) + existing_names = { + aff.name.lower() + if isinstance(aff, Affiliation) + else str(aff).lower() + for aff in existing_affs + } + for aff in gimie_author.affiliations: + aff_name = ( + aff.name.lower() + if isinstance(aff, Affiliation) + else str(aff).lower() + ) + if aff_name not in existing_names: + existing_author.setdefault( + "affiliations", + [], + ).append(aff) + updated = True + gimie_processed.add(gimie_author.id or gimie_author.name) + else: + # New author from GIMIE - add it + merged_authors.append(gimie_author) + logger.info( + f"Added new GIMIE author: {gimie_author.name} with {len(gimie_author.affiliations)} affiliations (id: {gimie_author.id})", + ) + gimie_processed.add(gimie_author.id or gimie_author.name) + + # Convert all merged authors to Person objects for consistency + final_authors = [] + for author in merged_authors: + if isinstance(author, Person): + final_authors.append(author) + elif isinstance(author, dict): + try: + author = self._sanitize_person_payload( + author, + "final merged author conversion", + ) + final_authors.append(Person(**author)) + except Exception as e: + logger.warning( + f"Failed to convert merged author dict to Person: {e}", + ) + # Keep as dict if conversion fails + final_authors.append(author) + else: + final_authors.append(author) + + if final_authors: + full_dict["author"] = final_authors + # Log affiliation counts + total_affs_after_merge = sum( + len(a.affiliations) + if isinstance(a, Person) and hasattr(a, "affiliations") + else 0 + for a in final_authors + ) + logger.info( + f"Merged {len(gimie_authors)} GIMIE authors with {len(existing_person_objects)} model authors, total: {len(final_authors)} authors with {total_affs_after_merge} total affiliations", + ) + + # name - prioritize GIMIE, then model + if "name" in gimie_dict and gimie_dict.get("name"): + full_dict["name"] = gimie_dict["name"] + logger.info(f"Using name from GIMIE: {gimie_dict['name']}") + elif "name" in simplified_dict and simplified_dict.get("name"): + full_dict["name"] = simplified_dict["name"] + + # applicationCategory + if "applicationCategory" in simplified_dict: + full_dict["applicationCategory"] = simplified_dict["applicationCategory"] + + # codeRepository - merge from model and GIMIE, convert strings to HttpUrl + # Prioritize GIMIE, then merge with model + code_repos = [] + if "codeRepository" in gimie_dict and gimie_dict.get("codeRepository"): + # Start with GIMIE repositories + code_repos.extend(gimie_dict["codeRepository"]) + logger.info( + f"Using codeRepository from GIMIE: {gimie_dict['codeRepository']}", + ) + + if "codeRepository" in simplified_dict and simplified_dict.get( + "codeRepository", + ): + # Merge model codeRepository, avoiding duplicates + for model_repo in simplified_dict["codeRepository"]: + if model_repo not in code_repos: + code_repos.append(model_repo) + logger.debug(f"Adding codeRepository from model: {model_repo}") + + if code_repos: + try: + full_dict["codeRepository"] = [ + HttpUrl(url) if not isinstance(url, HttpUrl) else url + for url in code_repos + ] + except Exception as e: + logger.warning(f"Failed to convert codeRepository URLs: {e}") + full_dict["codeRepository"] = [] + else: + # No codeRepository from either source + full_dict["codeRepository"] = [] + + # dateCreated - prioritize GIMIE, then git authors, then model + # Model should NOT be asked for dateCreated (it comes from GIMIE/git) + date_created = None + + # Priority 1: GIMIE (most reliable source) + if "dateCreated" in gimie_dict and gimie_dict.get("dateCreated"): + try: + date_created = date.fromisoformat(gimie_dict["dateCreated"]) + logger.info(f"Using dateCreated from GIMIE: {date_created}") + except (ValueError, TypeError) as e: + logger.warning(f"Failed to parse GIMIE dateCreated: {e}") + + # Priority 2: Oldest commit date from git authors + if date_created is None and git_authors: + oldest_date = None + for git_author in git_authors: + if isinstance(git_author, GitAuthor) and git_author.commits: + if git_author.commits.firstCommitDate: + if ( + oldest_date is None + or git_author.commits.firstCommitDate < oldest_date + ): + oldest_date = git_author.commits.firstCommitDate + elif isinstance(git_author, dict): + commits = git_author.get("commits") + if commits: + first_date = None + if isinstance(commits, dict): + first_date = commits.get("firstCommitDate") + elif hasattr(commits, "firstCommitDate"): + first_date = commits.firstCommitDate + + if first_date: + if isinstance(first_date, str): + try: + first_date = date.fromisoformat(first_date) + except (ValueError, TypeError): + continue + if oldest_date is None or first_date < oldest_date: + oldest_date = first_date + + if oldest_date: + date_created = oldest_date + logger.info(f"Using oldest commit date as dateCreated: {oldest_date}") + + # Priority 3: Model output (fallback only - model shouldn't be asked for this) + if ( + date_created is None + and "dateCreated" in simplified_dict + and simplified_dict.get("dateCreated") + ): + try: + date_created = date.fromisoformat(simplified_dict["dateCreated"]) + logger.info(f"Using dateCreated from model (fallback): {date_created}") + except (ValueError, TypeError): + logger.warning( + f"Failed to parse model dateCreated: {simplified_dict['dateCreated']}", + ) + + if date_created: + full_dict["dateCreated"] = date_created + + # license - merge from model and GIMIE (prefer model if both exist) + if "license" in simplified_dict and simplified_dict.get("license"): + full_dict["license"] = simplified_dict["license"] + elif "license" in gimie_dict and gimie_dict.get("license"): + full_dict["license"] = gimie_dict["license"] + + # gitAuthors - use extracted git_authors if available, otherwise convert from simplified_dict + if git_authors: + # Use the extracted git authors directly (they're already GitAuthor objects) + full_dict["gitAuthors"] = git_authors + logger.info( + f"Added {len(git_authors)} git authors from repository extraction", + ) + elif "gitAuthors" in simplified_dict and simplified_dict.get("gitAuthors"): + # Fallback: convert from simplified_dict if git_authors not provided + converted_git_authors = [] + for git_auth in simplified_dict["gitAuthors"]: + if not git_auth: + continue + git_author_dict = { + "name": git_auth.get("name", ""), + } + if git_auth.get("email"): + git_author_dict["email"] = git_auth["email"] + if git_auth.get("commits"): + commits_dict = git_auth["commits"] + commits_obj = { + "total": commits_dict.get( + "count", + commits_dict.get("total", 0), + ), + } + # Handle firstCommit/firstCommitDate (simplified model uses firstCommit as string) + first_commit = commits_dict.get("firstCommit") or commits_dict.get( + "firstCommitDate", + ) + if first_commit: + if isinstance(first_commit, str): + try: + commits_obj["firstCommitDate"] = date.fromisoformat( + first_commit, + ) + except (ValueError, TypeError): + logger.warning( + f"Failed to parse firstCommit date: {first_commit}", + ) + else: + commits_obj["firstCommitDate"] = first_commit + # Handle lastCommit/lastCommitDate + last_commit = commits_dict.get("lastCommit") or commits_dict.get( + "lastCommitDate", + ) + if last_commit: + if isinstance(last_commit, str): + try: + commits_obj["lastCommitDate"] = date.fromisoformat( + last_commit, + ) + except (ValueError, TypeError): + logger.warning( + f"Failed to parse lastCommit date: {last_commit}", + ) + else: + commits_obj["lastCommitDate"] = last_commit + git_author_dict["commits"] = commits_obj + converted_git_authors.append(git_author_dict) + if converted_git_authors: + full_dict["gitAuthors"] = converted_git_authors + + # discipline - convert strings to Discipline enum + if "discipline" in simplified_dict and simplified_dict.get("discipline"): + from ..data_models.models import Discipline + + disciplines = [] + for disc_str in simplified_dict["discipline"]: + try: + # Try to match enum value + for disc in Discipline: + if disc.value.lower() == disc_str.lower(): + disciplines.append(disc) + break + else: + # If no match, try to create from string + disciplines.append(Discipline(disc_str)) + except Exception: + logger.warning(f"Failed to convert discipline: {disc_str}") + full_dict["discipline"] = disciplines if disciplines else None + + # disciplineJustification + if "disciplineJustification" in simplified_dict: + full_dict["disciplineJustification"] = simplified_dict[ + "disciplineJustification" + ] + + # repositoryType - convert string to RepositoryType enum + if "repositoryType" in simplified_dict and simplified_dict.get( + "repositoryType", + ): + try: + repo_type_str = simplified_dict["repositoryType"] + for repo_type in RepositoryType: + if repo_type.value.lower() == repo_type_str.lower(): + full_dict["repositoryType"] = repo_type + break + else: + # Default to "other" if not found + full_dict["repositoryType"] = RepositoryType.OTHER + except Exception as e: + logger.warning(f"Failed to convert repositoryType: {e}") + full_dict["repositoryType"] = RepositoryType.OTHER + + # repositoryTypeJustification + if "repositoryTypeJustification" in simplified_dict: + full_dict["repositoryTypeJustification"] = simplified_dict[ + "repositoryTypeJustification" + ] + else: + full_dict["repositoryTypeJustification"] = [] + + # keywords - merge from GIMIE and model (prioritize GIMIE) + keywords_list = [] + if "keywords" in gimie_dict and gimie_dict.get("keywords"): + gimie_keywords = gimie_dict["keywords"] + if isinstance(gimie_keywords, list): + keywords_list.extend(gimie_keywords) + logger.info(f"Using keywords from GIMIE: {gimie_keywords}") + else: + keywords_list.append(gimie_keywords) + logger.info(f"Using keyword from GIMIE: {gimie_keywords}") + + if "keywords" in simplified_dict and simplified_dict.get("keywords"): + model_keywords = simplified_dict["keywords"] + if isinstance(model_keywords, list): + # Merge, avoiding duplicates + for kw in model_keywords: + if kw not in keywords_list: + keywords_list.append(kw) + logger.debug(f"Adding keyword from model: {kw}") + else: + if model_keywords not in keywords_list: + keywords_list.append(model_keywords) + logger.debug(f"Adding keyword from model: {model_keywords}") + + if keywords_list: + full_dict["keywords"] = keywords_list + logger.info(f"Final merged keywords: {keywords_list}") + else: + full_dict["keywords"] = [] + + # url - from GIMIE + if "url" in gimie_dict and gimie_dict.get("url"): + try: + full_dict["url"] = ( + HttpUrl(gimie_dict["url"]) + if not isinstance(gimie_dict["url"], HttpUrl) + else gimie_dict["url"] + ) + except Exception as e: + logger.warning(f"Failed to convert GIMIE url: {e}") + + # datePublished - merge from model and GIMIE (prefer model if both exist) + if "datePublished" in simplified_dict and simplified_dict.get("datePublished"): + try: + full_dict["datePublished"] = date.fromisoformat( + simplified_dict["datePublished"], + ) + except (ValueError, TypeError) as e: + logger.warning(f"Failed to parse model datePublished: {e}") + elif "datePublished" in gimie_dict and gimie_dict.get("datePublished"): + try: + date_pub_str = gimie_dict["datePublished"] + if isinstance(date_pub_str, str): + full_dict["datePublished"] = date.fromisoformat(date_pub_str) + logger.info( + f"Using datePublished from GIMIE: {full_dict['datePublished']}", + ) + except (ValueError, TypeError) as e: + logger.warning(f"Failed to parse GIMIE datePublished: {e}") + + # dateModified - from GIMIE + if "dateModified" in gimie_dict and gimie_dict.get("dateModified"): + try: + date_mod_str = gimie_dict["dateModified"] + if isinstance(date_mod_str, str): + full_dict["dateModified"] = date.fromisoformat(date_mod_str) + logger.info( + f"Using dateModified from GIMIE: {full_dict['dateModified']}", + ) + except (ValueError, TypeError) as e: + logger.warning(f"Failed to parse GIMIE dateModified: {e}") + + # programmingLanguage - merge from model and GIMIE + prog_langs = [] + if "programmingLanguage" in simplified_dict and simplified_dict.get( + "programmingLanguage", + ): + model_langs = simplified_dict["programmingLanguage"] + if isinstance(model_langs, list): + prog_langs.extend(model_langs) + else: + prog_langs.append(model_langs) + + if "programmingLanguage" in gimie_dict and gimie_dict.get( + "programmingLanguage", + ): + gimie_langs = gimie_dict["programmingLanguage"] + if isinstance(gimie_langs, list): + for lang in gimie_langs: + if lang not in prog_langs: + prog_langs.append(lang) + else: + if gimie_langs not in prog_langs: + prog_langs.append(gimie_langs) + + if prog_langs: + full_dict["programmingLanguage"] = prog_langs + + # readme - from GIMIE + if "readme" in gimie_dict and gimie_dict.get("readme"): + try: + full_dict["readme"] = ( + HttpUrl(gimie_dict["readme"]) + if not isinstance(gimie_dict["readme"], HttpUrl) + else gimie_dict["readme"] + ) + except Exception as e: + logger.warning(f"Failed to convert GIMIE readme URL: {e}") + + # citation - merge from model and GIMIE + citations = [] + if "citation" in simplified_dict and simplified_dict.get("citation"): + model_citations = simplified_dict["citation"] + if isinstance(model_citations, list): + citations.extend(model_citations) + else: + citations.append(model_citations) + + if "citation" in gimie_dict and gimie_dict.get("citation"): + gimie_citations = gimie_dict["citation"] + if isinstance(gimie_citations, list): + for cit in gimie_citations: + if cit not in citations: + citations.append(cit) + else: + if gimie_citations not in citations: + citations.append(gimie_citations) + + if citations: + try: + full_dict["citation"] = [ + HttpUrl(cit) if not isinstance(cit, HttpUrl) else cit + for cit in citations + ] + except Exception as e: + logger.warning(f"Failed to convert citation URLs: {e}") + + # Convert SimplifiedOrganization objects to full Organization objects + if "relatedToOrganizations" in simplified_dict and simplified_dict.get( + "relatedToOrganizations", + ): + organizations = [] + for org_data in simplified_dict["relatedToOrganizations"]: + # Convert dict to Organization if needed + if isinstance(org_data, dict): + # Map SimplifiedOrganization.name to Organization.legalName + if "name" in org_data and "legalName" not in org_data: + org_data["legalName"] = org_data.pop("name") + + # Ensure type is set + if "type" not in org_data: + org_data["type"] = "Organization" + # Set source if not present + if "source" not in org_data: + org_data["source"] = "atomic_agent" + # Ensure id is set (use legalName as fallback if no id) + if "id" not in org_data or not org_data["id"]: + org_data["id"] = org_data.get("legalName", "") + + try: + org = Organization(**org_data) + organizations.append(org) + except Exception as e: + logger.warning(f"Failed to create Organization from dict: {e}") + continue + elif isinstance(org_data, Organization): + organizations.append(org_data) + else: + logger.warning( + f"Unexpected organization data type: {type(org_data)}", + ) + + if organizations: + full_dict["relatedToOrganizations"] = organizations + logger.info( + f"Converted {len(organizations)} organizations to full Organization objects", + ) + + # Pass through relatedToOrganizationJustification if present + if "relatedToOrganizationJustification" in simplified_dict: + full_dict["relatedToOrganizationJustification"] = simplified_dict[ + "relatedToOrganizationJustification" + ] + + return full_dict def run_authors_enrichment(self): + """ + Enrich authors with ORCID affiliations. + + This runs after LLM analysis and enriches Person objects that have ORCID IDs + with affiliation data from ORCID API. Uses the Affiliation model with source="orcid". + """ logger.info(f"ORCID enrichment for {self.full_path}") # Check if data exists before enrichment @@ -117,6 +2168,7 @@ def run_authors_enrichment(self): if isinstance(llm_result, SoftwareSourceCode): self.data = llm_result + logger.info(f"ORCID enrichment successful for {self.full_path}") else: logging.warning(f"Author enrichment failed for {self.full_path}") @@ -162,7 +2214,9 @@ async def run_organization_enrichment(self): # Replace relatedToOrganizations with enriched Organization objects only # Don't add both org name strings and Organization objects - just objects - self.data.relatedToOrganizations = list(enriched_orgs) if enriched_orgs else None + self.data.relatedToOrganizations = ( + list(enriched_orgs) if enriched_orgs else None + ) # These values are overwritten only if provided by the enrichment if organization_enrichment.relatedToEPFL is not None: @@ -279,7 +2333,6 @@ async def run_user_enrichment(self): self.estimated_input_tokens += usage.get("estimated_input_tokens", 0) self.estimated_output_tokens += usage.get("estimated_output_tokens", 0) - def _names_match(self, name1: str, name2: str) -> bool: """ Check if two names match, handling variations like: @@ -312,148 +2365,268 @@ def normalize_name(name): else: return n2_parts.issubset(n1_parts) - async def run_academic_catalog_enrichment(self): - """Enrich repository with academic catalog relations (Infoscience, etc.)""" - logger.info(f"Academic catalog enrichment for {self.full_path}") + async def run_linked_entities_enrichment(self): + """ + Enrich repository with linked entities relations using atomic pipeline. + + This uses a two-stage atomic pipeline: + 1. Search academic catalogs (Infoscience) with tools for repository and authors + 2. Structure the search results into organized relations + """ + logger.info(f"linked entities enrichment for {self.full_path}") # Check if data exists before enrichment if self.data is None: logger.warning( - f"Cannot enrich academic catalogs: no data available for {self.full_path}", + f"Cannot enrich linked entities: no data available for {self.full_path}", ) return try: - # Extract repository information for the enrichment + # Extract repository name from existing data repository_name = self.data.name or self.full_path.split("/")[-1] - description = self.data.description or "" - - # Get README excerpt (first 1000 chars from readme content if available) - readme_excerpt = "" - if hasattr(self.data, "readme") and self.data.readme: - # The readme field is a URL, we'd need to fetch it - # For now, use description or other text - pass - - # Try to get some text from description or other fields - if self.data.description: - readme_excerpt = self.data.description[:1000] - elif ( - hasattr(self.data, "hasExecutableInstructions") - and self.data.hasExecutableInstructions - ): - readme_excerpt = self.data.hasExecutableInstructions[:1000] - - # Extract author names and organization names from existing data - author_names = [] - organization_names = [] - - if hasattr(self.data, "author") and self.data.author: - for author in self.data.author: - if hasattr(author, "name") and author.name: - author_names.append(author.name) - elif hasattr(author, "legalName") and author.legalName: - organization_names.append(author.legalName) - - # Also check relatedToOrganizations for Organization objects - if self.data.relatedToOrganizations: - for org in self.data.relatedToOrganizations: - if isinstance(org, Organization) and hasattr(org, "legalName") and org.legalName: - if org.legalName not in organization_names: - organization_names.append(org.legalName) - - result = await enrich_repository_academic_catalog( - repository_url=self.full_path, + + logger.info( + f"Searching Infoscience for repository: '{repository_name}'", + ) + + # Stage 1: Search academic catalogs with tools (max 5 results per search) + logger.info( + "Stage 1: Searching academic catalogs (repository-level only)...", + ) + search_result = await search_academic_catalogs( repository_name=repository_name, - description=description, - readme_excerpt=readme_excerpt, - authors=author_names, - organizations=organization_names, ) - # Extract data and usage - enrichment_data = result.get("data") if isinstance(result, dict) else result - usage = result.get("usage") if isinstance(result, dict) else None + search_context = search_result.get("data") + usage = search_result.get("usage") - # Accumulate token usage + if not search_context: + logger.error("Academic catalog search failed") + return + + # Accumulate usage from stage 1 if usage: self.total_input_tokens += usage.get("input_tokens", 0) self.total_output_tokens += usage.get("output_tokens", 0) - logger.info( - f"Academic catalog enrichment usage: {usage.get('input_tokens', 0)} input, " - f"{usage.get('output_tokens', 0)} output tokens", + if "estimated_input_tokens" in usage: + self.estimated_input_tokens += usage.get( + "estimated_input_tokens", + 0, + ) + self.estimated_output_tokens += usage.get( + "estimated_output_tokens", + 0, + ) + + # Stage 2: Structure the search results + logger.info( + "Stage 2: Structuring linked entities results (repository-level only)...", + ) + + # Generate a simplified schema for the structured output + # Note: The simplified model is generated dynamically in linked_entities_searcher.py + schema = { + "repository_relations": { + "type": "array", + "description": "Publications/entities about the repository itself", + }, + } + + structure_result = await structure_linked_entities( + search_context=search_context, + schema=schema, + ) + + enrichment_data = structure_result.get("data") + usage = structure_result.get("usage") + + if not enrichment_data: + logger.error("Linked entities structuring failed") + return + + # Validate enrichment_data type + if isinstance(enrichment_data, str): + logger.error( + f"Enrichment data is a string (unexpected): {enrichment_data[:200]}", ) + return - if usage and "estimated_input_tokens" in usage: - self.estimated_input_tokens += usage.get("estimated_input_tokens", 0) - self.estimated_output_tokens += usage.get("estimated_output_tokens", 0) + # Convert to dict if it's a Pydantic model + if hasattr(enrichment_data, "model_dump"): + enrichment_dict = enrichment_data.model_dump() + elif isinstance(enrichment_data, dict): + enrichment_dict = enrichment_data + else: + logger.error( + f"Unexpected enrichment_data type: {type(enrichment_data)}", + ) + return + + logger.info( + f"Structured linked entities result with keys: {list(enrichment_dict.keys())}", + ) + + # Accumulate usage from stage 2 + if usage: + self.total_input_tokens += usage.get("input_tokens", 0) + self.total_output_tokens += usage.get("output_tokens", 0) + if "estimated_input_tokens" in usage: + self.estimated_input_tokens += usage.get( + "estimated_input_tokens", + 0, + ) + self.estimated_output_tokens += usage.get( + "estimated_output_tokens", + 0, + ) + + # Store the linked entities relations at repository level + if enrichment_dict: + # Debug: Log what we got + logger.info(f"Enrichment dict keys: {list(enrichment_dict.keys())}") + logger.info( + f"Repository relations count: {len(enrichment_dict.get('repository_relations', []))}", + ) + logger.info( + f"Author relations count: {len(enrichment_dict.get('author_relations', {}))}", + ) - # Store the academic catalog relations at repository level - if enrichment_data: # Repository-level relations (publications about the repository itself) - if hasattr(enrichment_data, "repository_relations"): - self.data.academicCatalogRelations = ( - enrichment_data.repository_relations + if "repository_relations" in enrichment_dict: + # Convert simplified relations to full relations + from ..data_models import linkedEntitiesRelation + + repo_relations = [] + repo_rels_list = enrichment_dict.get("repository_relations", []) + logger.info( + f"Processing {len(repo_rels_list)} repository relations...", ) + + for idx, simplified_rel in enumerate(repo_rels_list): + # Skip if it's a string (shouldn't happen, but handle gracefully) + if isinstance(simplified_rel, str): + logger.warning( + f"Skipping repository relation {idx}: got string instead of dict", + ) + continue + + # Convert to dict + if hasattr(simplified_rel, "model_dump"): + rel_dict = simplified_rel.model_dump() + elif isinstance(simplified_rel, dict): + rel_dict = simplified_rel + else: + logger.warning( + f"Skipping repository relation {idx}: unexpected type {type(simplified_rel)}", + ) + continue + + # Reconcile Union fields (entity split into entityInfosciencePublication, entityInfoscienceAuthor, entityInfoscienceOrgUnit) + rel_dict = self._reconcile_entity_union(rel_dict) + + try: + repo_relations.append(linkedEntitiesRelation(**rel_dict)) + except Exception as e: + logger.warning( + f"Failed to create linkedEntitiesRelation: {e}", + ) + continue + + self.data.linkedEntities = repo_relations logger.info( - f"Stored {len(enrichment_data.repository_relations)} repository-level academic catalog relations", + f"✓ Stored {len(repo_relations)} repository-level linked entities relations", ) - # Fallback for backward compatibility - elif hasattr(enrichment_data, "relations"): - self.data.academicCatalogRelations = enrichment_data.relations + else: + logger.warning( + "No 'repository_relations' key found in enrichment_dict", + ) + self.data.linkedEntities = [] + + # Note: Author-level linked entities are handled in optional enrichment + # See run_author_linked_entities_enrichment() for per-author Infoscience searches + + except Exception as e: + logger.error(f"linked entities enrichment failed: {e}", exc_info=True) + # Don't fail the entire analysis, just skip linked entities enrichment + return + + async def run_author_linked_entities_enrichment(self): + """ + Optional enrichment: Search Infoscience for each author individually. + + This is separate from the main atomic pipeline and runs only when requested. + Assigns linkedEntities to each Person in self.data.author. + """ + logger.info(f"Author-level linked entities enrichment for {self.full_path}") + + # Check if data exists + if self.data is None: + logger.warning( + f"Cannot enrich author linked entities: no data available for {self.full_path}", + ) + return + + # Check if we have authors + if not hasattr(self.data, "author") or not self.data.author: + logger.info("No authors to enrich with linked entities") + return + + try: + # Import Infoscience tools + from ..context.infoscience import ( + search_infoscience_authors_tool, + ) + + # Search for each author + for author in self.data.author: + if not hasattr(author, "name") or not author.name: + continue + + logger.info(f"Searching Infoscience for author: {author.name}") + + # Search for author profile and publications + try: + # Search for author profile + author_results = await search_infoscience_authors_tool( + name=author.name, + max_results=5, + ) + + # Parse results and create linkedEntitiesRelation objects + # Note: This is a simplified direct search, not via atomic pipeline + # Results parsing would need to be implemented based on tool output format + + # For now, log that we searched logger.info( - f"Stored {len(enrichment_data.relations)} academic catalog relations at repository level", - ) - - # Directly assign relations to authors and organizations using the structured output - if hasattr(self.data, "author") and self.data.author: - # For Person objects - match by author name - if hasattr(enrichment_data, "author_relations"): - for author in self.data.author: - if hasattr(author, "name") and author.name: - # Direct lookup using the exact author name as key - if author.name in enrichment_data.author_relations: - author_rels = enrichment_data.author_relations[ - author.name - ] - author.academicCatalogRelations = author_rels - logger.info( - f"✓ Directly assigned {len(author_rels)} relations to author: {author.name}", - ) - else: - # Author had no results - author.academicCatalogRelations = [] - logger.info( - f"ℹ No relations found for author: {author.name}", - ) + f"Searched Infoscience for {author.name}: {len(author_results) if author_results else 0} results", + ) - # For Organization objects in the author list - match by legalName - elif hasattr(author, "legalName") and author.legalName: - if ( - hasattr(enrichment_data, "organization_relations") - and author.legalName - in enrichment_data.organization_relations - ): - org_rels = enrichment_data.organization_relations[ - author.legalName - ] - author.academicCatalogRelations = org_rels - logger.info( - f"✓ Directly assigned {len(org_rels)} relations to organization: {author.legalName}", - ) - else: - author.academicCatalogRelations = [] - logger.info( - f"ℹ No relations found for organization: {author.legalName}", - ) + # TODO: Parse author_results and create linkedEntitiesRelation objects + # author.linkedEntities = [...] + + except Exception as e: + logger.warning( + f"Failed to search Infoscience for author {author.name}: {e}", + ) + continue except Exception as e: - logger.error(f"Academic catalog enrichment failed: {e}", exc_info=True) - # Don't fail the entire analysis, just skip academic catalog enrichment + logger.error( + f"Author linked entities enrichment failed: {e}", + exc_info=True, + ) + # Don't fail the entire analysis return async def run_epfl_final_assessment(self): - """Run final EPFL relationship assessment after all enrichments complete""" + """ + Run final EPFL relationship assessment using atomic pipeline after all enrichments complete. + + This uses a two-stage atomic pipeline: + 1. Compile enriched data into markdown context + 2. Assess EPFL relationship from compiled context + """ logger.info(f"Final EPFL assessment for {self.full_path}") # Check if data exists @@ -464,31 +2637,63 @@ async def run_epfl_final_assessment(self): return try: - # Convert data to dict for assessment - data_dict = self.data.model_dump() + # Convert data to dict for compilation + enriched_data_dict = self.data.model_dump() - # Call the EPFL assessment agent - result = await assess_epfl_relationship( - data=data_dict, - item_type="repository", + # Stage 1: Compile enriched data into markdown context + logger.info("Stage 1: Compiling enriched data for EPFL assessment...") + compilation_result = await compile_enriched_data_for_epfl( + enriched_data=enriched_data_dict, + repository_url=self.full_path, ) - # Extract assessment and usage - assessment = result.get("data") if isinstance(result, dict) else result - usage = result.get("usage") if isinstance(result, dict) else None + enriched_context = compilation_result.get("data") + usage = compilation_result.get("usage") + + if not enriched_context: + logger.error("Enriched data compilation failed") + return - # Accumulate token usage + # Accumulate usage from stage 1 if usage: self.total_input_tokens += usage.get("input_tokens", 0) self.total_output_tokens += usage.get("output_tokens", 0) - logger.info( - f"EPFL assessment usage: {usage.get('input_tokens', 0)} input, {usage.get('output_tokens', 0)} output tokens", - ) + if "estimated_input_tokens" in usage: + self.estimated_input_tokens += usage.get( + "estimated_input_tokens", + 0, + ) + self.estimated_output_tokens += usage.get( + "estimated_output_tokens", + 0, + ) - # Accumulate estimated tokens - if usage and "estimated_input_tokens" in usage: - self.estimated_input_tokens += usage.get("estimated_input_tokens", 0) - self.estimated_output_tokens += usage.get("estimated_output_tokens", 0) + # Stage 2: Assess EPFL relationship from compiled context + logger.info("Stage 2: Assessing EPFL relationship...") + assessment_result = await assess_final_epfl_relationship( + enriched_context=enriched_context, + ) + + assessment = assessment_result.get("data") + usage = assessment_result.get("usage") + + if not assessment: + logger.error("EPFL assessment failed") + return + + # Accumulate usage from stage 2 + if usage: + self.total_input_tokens += usage.get("input_tokens", 0) + self.total_output_tokens += usage.get("output_tokens", 0) + if "estimated_input_tokens" in usage: + self.estimated_input_tokens += usage.get( + "estimated_input_tokens", + 0, + ) + self.estimated_output_tokens += usage.get( + "estimated_output_tokens", + 0, + ) # Update data with final assessment (overwrite previous values) self.data.relatedToEPFL = assessment.relatedToEPFL @@ -590,9 +2795,9 @@ def dump_results(self, output_type="json") -> str | dict | None: if output_type == "pydantic": return self.data elif output_type == "json": - return self.data.model_dump_json(indent=2) + return self.data.model_dump_json(indent=2, exclude_none=True) elif output_type == "dict": - return self.data.model_dump() + return self.data.model_dump(exclude_none=True) elif output_type == "json-ld": return self.data.convert_pydantic_to_jsonld() else: @@ -609,6 +2814,7 @@ async def run_analysis( run_llm: bool = True, run_user_enrichment: bool = True, run_organization_enrichment: bool = True, + run_author_linked_entities: bool = False, ): """ Run the full analysis pipeline with optional steps. @@ -645,40 +2851,58 @@ async def run_analysis( if run_llm: logging.info(f"LLM analysis for {self.full_path}") await self.run_llm_analysis() + logging.info(f"LLM analysis completed for {self.full_path}") - # Only run author enrichment if LLM analysis succeeded + # Run ORCID enrichment after LLM analysis (enriches authors with ORCID IDs) if self.data is not None: + logging.info(f"ORCID enrichment for {self.full_path}") self.run_authors_enrichment() + logging.info(f"ORCID enrichment completed for {self.full_path}") else: logging.warning( - f"Skipping author enrichment: LLM analysis failed for {self.full_path}", + f"Skipping ORCID enrichment: LLM analysis failed for {self.full_path}", ) - logging.info(f"LLM analysis completed for {self.full_path}") - - # Run user enrichment - if run_user_enrichment and self.data is not None: - logging.info(f"User enrichment for {self.full_path}") - await self.run_user_enrichment() - logging.info(f"User enrichment completed for {self.full_path}") + # Run user enrichment + if run_user_enrichment and self.data is not None: + logging.info(f"User enrichment for {self.full_path}") + await self.run_user_enrichment() + logging.info(f"User enrichment completed for {self.full_path}") + # Deduplicate authors after user enrichment + self._deduplicate_authors() + + # Run organization enrichment + if run_organization_enrichment and self.data is not None: + logging.info(f"Organization enrichment for {self.full_path}") + await self.run_organization_enrichment() + logging.info(f"Organization enrichment completed for {self.full_path}") + + # Run academic catalog linked entities enrichment (atomic pipeline with tools) + if self.data is not None: + logging.info(f"Academic catalog enrichment for {self.full_path}") + await self.run_linked_entities_enrichment() + logging.info( + f"Academic catalog enrichment completed for {self.full_path}", + ) - # Run organization enrichment - if run_organization_enrichment and self.data is not None: - logging.info(f"Organization enrichment for {self.full_path}") - await self.run_organization_enrichment() - logging.info(f"Organization enrichment completed for {self.full_path}") + # Run optional per-author linked entities enrichment + if run_author_linked_entities: + logging.info( + f"Author-level linked entities enrichment for {self.full_path}", + ) + await self.run_author_linked_entities_enrichment() + logging.info( + f"Author-level linked entities enrichment completed for {self.full_path}", + ) - # Run academic catalog enrichment - if self.data is not None: - logging.info(f"Academic catalog enrichment for {self.full_path}") - await self.run_academic_catalog_enrichment() - logging.info(f"Academic catalog enrichment completed for {self.full_path}") + # Deduplicate authors after linked entities enrichment + self._deduplicate_authors() - # Run final EPFL assessment after all enrichments complete - if self.data is not None: - logging.info(f"Final EPFL assessment for {self.full_path}") - await self.run_epfl_final_assessment() - logging.info(f"Final EPFL assessment completed for {self.full_path}") + # Run final EPFL assessment after all enrichments complete (atomic pipeline) + if self.data is not None: + logging.info(f"Final EPFL assessment for {self.full_path}") + await self.run_epfl_final_assessment() + logging.info(f"Final EPFL assessment completed for {self.full_path}") # Only validate and cache if we have data if self.data is not None: @@ -692,7 +2916,35 @@ async def run_analysis( # Track end time self.end_time = datetime.now() - # Log duration + # Log duration and final token usage summary if self.start_time and self.end_time: duration = (self.end_time - self.start_time).total_seconds() - logging.info(f"Analysis completed in {duration:.2f} seconds") + + # Final token usage summary + logger.info("") + logger.info("=" * 80) + logger.info("FINAL TOKEN USAGE SUMMARY (All Stages)") + logger.info("=" * 80) + logger.info(" Official API Counts:") + logger.info(f" Input tokens: {self.total_input_tokens:,}") + logger.info(f" Output tokens: {self.total_output_tokens:,}") + logger.info( + f" Total tokens: {self.total_input_tokens + self.total_output_tokens:,}", + ) + logger.info("") + logger.info(" Estimated Counts (tiktoken):") + logger.info(f" Input tokens: {self.estimated_input_tokens:,}") + logger.info(f" Output tokens: {self.estimated_output_tokens:,}") + logger.info( + f" Total tokens: {self.estimated_input_tokens + self.estimated_output_tokens:,}", + ) + logger.info("") + if self.total_input_tokens == 0 and self.total_output_tokens == 0: + logger.warning( + " ⚠️ API returned 0 tokens - using tiktoken estimates as primary metric", + ) + logger.info(f" Analysis Duration: {duration:.2f} seconds") + logger.info( + f" Status: {'SUCCESS' if self.analysis_successful else 'FAILED'}", + ) + logger.info("=" * 80) diff --git a/src/analysis/user.py b/src/analysis/user.py index 51abadf..8385994 100644 --- a/src/analysis/user.py +++ b/src/analysis/user.py @@ -2,8 +2,8 @@ from datetime import datetime from ..agents import llm_request_user_infos -from ..agents.academic_catalog_enrichment import enrich_user_academic_catalog from ..agents.epfl_assessment import assess_epfl_relationship +from ..agents.linked_entities_enrichment import enrich_user_linked_entities from ..agents.organization_enrichment import enrich_organizations_from_dict from ..agents.user_enrichment import enrich_users_from_dict from ..cache.cache_manager import CacheManager, get_cache_manager @@ -42,12 +42,17 @@ def run_github_parsing(self): # Parse GitHub user metadata github_metadata = parse_github_user(self.username) + if github_metadata is None: + logger.error(f"Failed to parse GitHub user metadata for {self.username}") + return + # Convert GitHubUserMetadata to dict and merge into self.data user_data_dict = github_metadata.model_dump() # Map GitHubUserMetadata fields to GitHubUser model self.data = GitHubUser( # Basic fields + id=f"https://github.com/{self.username}", name=user_data_dict.get("name"), fullname=user_data_dict.get("name"), # Use name as fullname by now githubHandle=user_data_dict.get("login"), @@ -90,6 +95,7 @@ async def run_llm_analysis(self): "public_repos": github_metadata.get("public_repos"), "followers": github_metadata.get("followers"), "following": github_metadata.get("following"), + "repositories": github_metadata.get("repositories", []), } try: @@ -97,7 +103,7 @@ async def run_llm_analysis(self): result = await llm_request_user_infos( username=self.username, user_data=llm_input_data, - max_tokens=20000, + max_tokens=10000, ) # Extract data and usage @@ -207,8 +213,7 @@ async def run_organization_enrichment(self): # Add user as author if we have ORCID data if github_metadata.get("orcid"): author_data = { - "name": github_metadata.get("name") - or self.data.fullname, + "name": github_metadata.get("name") or self.data.fullname, "orcid": github_metadata.get("orcid"), "affiliation": github_metadata.get("organizations", []), } @@ -305,8 +310,7 @@ async def run_user_enrichment(self): # Build existing author data using the new model structure if self.data.fullname or github_metadata.get("name"): author_data = { - "name": self.data.fullname - or github_metadata.get("name"), + "name": self.data.fullname or github_metadata.get("name"), "orcid": github_metadata.get("orcid"), "affiliation": github_metadata.get("organizations", []), } @@ -349,7 +353,7 @@ async def run_user_enrichment(self): logger.info(f"User enrichment completed for {self.username}") - async def run_academic_catalog_enrichment(self): + async def run_linked_entities_enrichment(self): """Enrich user with academic catalog relations (Infoscience, etc.)""" logger.info(f"Academic catalog enrichment for {self.username}") @@ -372,11 +376,12 @@ async def run_academic_catalog_enrichment(self): bio = github_metadata.get("bio", "") organizations = github_metadata.get("organizations", []) - result = await enrich_user_academic_catalog( + result = await enrich_user_linked_entities( username=self.username, full_name=full_name, bio=bio, organizations=organizations, + force_refresh=self.force_refresh, ) # Extract data and usage @@ -398,7 +403,7 @@ async def run_academic_catalog_enrichment(self): # Store the academic catalog relations if enrichment_data and hasattr(enrichment_data, "relations"): - self.data.academicCatalogRelations = enrichment_data.relations + self.data.linkedEntities = enrichment_data.relations logger.info( f"Stored {len(enrichment_data.relations)} academic catalog relations", ) @@ -609,7 +614,7 @@ async def run_analysis( # Run academic catalog enrichment if self.data is not None: logging.info(f"Academic catalog enrichment for {self.username}") - await self.run_academic_catalog_enrichment() + await self.run_linked_entities_enrichment() logging.info(f"Academic catalog enrichment completed for {self.username}") # Run final EPFL assessment after all enrichments complete diff --git a/src/api.py b/src/api.py index cd56a03..31a47a2 100644 --- a/src/api.py +++ b/src/api.py @@ -174,7 +174,7 @@ def index(): Returns basic information about the API version, GIMIE version, and configured LLM model. """ return { - "title": f"Hello, welcome to the Git Metadata Extractor v2.0.0. Gimie Version 0.7.2. LLM Model {os.environ['MODEL']}", + "title": f"Hello, welcome to the Git Metadata Extractor v2.0.0. Gimie Version 0.7.2. LLM Model {os.environ.get('MODEL', 'N/A (configured via model configs)')}", } @@ -555,22 +555,32 @@ async def get_org_json( github_info: dict = Depends(validate_github_token), ) -> APIOutput: """ - Retrieve and enrich GitHub organization metadata. - - Fetches organization profile from GitHub API and enriches it using LLM - to extract additional insights and structured information. - - **LLM Analysis**: - Uses PydanticAI to analyze the organization profile and extract: - - Organization type (academic, research, industry, non-profit, etc.) - - Scientific/technical disciplines - - EPFL relationship analysis + Retrieve and enrich GitHub organization metadata using atomic agents pipeline. + + Fetches organization profile from GitHub API and enriches it using a multi-stage + atomic agents pipeline to extract structured metadata and relationships. + + **Atomic Agents Pipeline** (6 stages): + 1. **Context Compilation**: Gathers comprehensive organization information using tools: + - Infoscience labs/orgunits search (EPFL organizational units) + - Infoscience publications search (related publications) + - Web search (DuckDuckGo) for additional context + - Compiles all information into structured markdown + 2. **Structured Output**: Extracts basic identity fields (name, description) from compiled context + 3. **Classification**: Classifies organization type and scientific disciplines with justifications: + - Organization type (Research Institute, University, Company, etc.) + - Scientific disciplines (from closed list of valid disciplines) + 4. **Organization Identifier**: Identifies related organizations (parent, partner, affiliated organizations) + 5. **Linked Entities**: Searches academic catalogs (Infoscience) for: + - Organizational units (orgunit) matching the organization + - Publications related to the organization + - Publications by organization members + 6. **EPFL Assessment**: Final holistic assessment of EPFL relationship with confidence scoring **Organization Enrichment** (optional): - When `enrich_orgs=true`, performs a second-pass agentic analysis using PydanticAI to: - - Query ROR (Research Organization Registry) for standardized organization names and IDs + When `enrich_orgs=true`, performs ROR (Research Organization Registry) enrichment to: + - Query ROR API for standardized organization names and IDs - Identify hierarchical relationships (departments, labs within universities) - - Provide detailed EPFL relationship analysis with evidence - Enrich organization metadata with type, country, website, etc. **Caching**: Results are cached with TTL of 365 days. @@ -578,13 +588,14 @@ async def get_org_json( **Parameters**: - **full_path**: GitHub organization URL or path (e.g., `https://github.com/organization`) - **force_refresh**: Set to `true` to bypass cache and fetch fresh data - - **enrich_orgs**: Set to `true` to enable organization enrichment with PydanticAI agent + - **enrich_orgs**: Set to `true` to enable ROR-based organization enrichment **Returns**: - Organization link - Organization type - Parsing timestamp - Organization Object with enriched metadata + - Usage statistics (token counts, timing, status) """ org_name = full_path.split("/")[-1] @@ -666,8 +677,25 @@ async def get_user_json( """ Retrieve and enrich GitHub user profile metadata. - Fetches user profile from GitHub API and enriches it using LLM - to extract additional insights, research interests, and structured information. + Uses a multi-stage atomic agent pipeline to extract and enrich user information: + 1. **Context Compiler**: Gathers user information using tools (ORCID, Infoscience authors/labs, web search) and compiles into markdown + 2. **Structured Output**: Extracts basic identity fields (name, fullname, githubHandle) + 3. **Discipline/Position Classifier**: Classifies user's discipline(s) and position(s) with justifications (using closed list of disciplines) + 4. **Organization Identifier**: Identifies related organizations (reuses repository's organization identification logic) + 5. **Linked Entities Searcher**: Searches Infoscience for persona (user) and orgunit (organizations) entities + 6. **EPFL Assessment**: Final holistic assessment of EPFL relationship (runs after all enrichments) + + **Context Compiler Tools**: + - ORCID search for author information and affiliations + - Infoscience author search (persona) for EPFL researchers + - Infoscience lab search (orgunit) for EPFL labs and organizational units + - Web search for additional context + - Author publications retrieval from Infoscience + + **Linked Entities Enhancement**: + - When searching for orgunit (labs), includes user's name in search queries + - Some labs use GitHub user profiles, so searching with both lab name and user name helps find them + - Searches both persona (user) and orgunit (organizations) in Infoscience **Organization Enrichment** (optional): When `enrich_orgs=true`, performs a second-pass agentic analysis using PydanticAI to: @@ -695,7 +723,8 @@ async def get_user_json( - User profile link - User type - Parsing timestamp - - User Object with enriched metadata + - User Object with enriched metadata (id field set to full GitHub profile URL) + - Statistics (token usage, timing, and status) """ username = full_path.split("/")[-1] diff --git a/src/context/infoscience.py b/src/context/infoscience.py index 82b39d6..5d693e7 100644 --- a/src/context/infoscience.py +++ b/src/context/infoscience.py @@ -13,7 +13,7 @@ from ..data_models.infoscience import ( InfoscienceAuthor, - InfoscienceLab, + InfoscienceOrgUnit, InfosciencePublication, InfoscienceSearchResult, ) @@ -32,6 +32,13 @@ _search_cache: Dict[str, str] = {} +def clear_infoscience_cache(): + """Clear the in-memory Infoscience search cache.""" + global _search_cache + _search_cache.clear() + logger.info("Cleared Infoscience search cache") + + ########################################################## # HTTP Client Functions ########################################################## @@ -199,25 +206,35 @@ def _parse_author(item: Dict[str, Any]) -> Optional[InfoscienceAuthor]: elif handle: url = f"https://infoscience.epfl.ch/record/{handle}" + # Extract email, ORCID, and affiliation + email = _parse_metadata(metadata, "eperson.email") + orcid = _parse_metadata(metadata, "person.identifier.orcid") + affiliation = _parse_metadata(metadata, "person.affiliation.name") + + # Log what we found for debugging + logger.debug( + f"Parsed author '{name}' - UUID: {uuid}, Email: {email}, ORCID: {orcid}, Affiliation: {affiliation}", + ) + return InfoscienceAuthor( uuid=uuid, name=name, - email=_parse_metadata(metadata, "eperson.email"), - orcid=_parse_metadata(metadata, "person.identifier.orcid"), - affiliation=_parse_metadata(metadata, "person.affiliation.name"), + email=email, + orcid=orcid, + affiliation=affiliation, profile_url=url, # Fixed: use profile_url instead of url ) -def _parse_lab(item: Dict[str, Any]) -> Optional[InfoscienceLab]: +def _parse_lab(item: Dict[str, Any]) -> Optional[InfoscienceOrgUnit]: """ - Parse a DSpace organizational unit entity into an InfoscienceLab model. + Parse a DSpace organizational unit entity into an InfoscienceOrgUnit model. Args: item: DSpace orgunit item dictionary Returns: - InfoscienceLab instance or None if parsing fails + InfoscienceOrgUnit instance or None if parsing fails """ metadata = item.get("metadata", {}) uuid = item.get("uuid") @@ -242,13 +259,16 @@ def _parse_lab(item: Dict[str, Any]) -> Optional[InfoscienceLab]: elif handle: url = f"https://infoscience.epfl.ch/record/{handle}" - return InfoscienceLab( + return InfoscienceOrgUnit( uuid=uuid, name=name, description=_parse_metadata(metadata, "dc.description") or _parse_metadata(metadata, "dc.description.abstract"), url=url, - parent_organization=_parse_metadata(metadata, "organization.parentOrganization"), + parent_organization=_parse_metadata( + metadata, + "organization.parentOrganization", + ), ) @@ -418,7 +438,7 @@ async def search_authors( authors.append( InfoscienceAuthor( name=author_name, - ) + ), ) except Exception as e: logger.warning(f"Error extracting authors from publication: {e}") @@ -538,7 +558,7 @@ async def search_labs( pub_title = _parse_metadata(metadata, "dc.title") description = f"Lab identified from publication: {pub_title[:100] if pub_title else 'N/A'}..." - lab = InfoscienceLab( + lab = InfoscienceOrgUnit( name=lab_info, description=description, ) @@ -643,7 +663,7 @@ async def search_infoscience_publications_tool( IMPORTANT: This tool caches results - don't search for the same thing multiple times! Be strategic and avoid redundant searches. - + **CRITICAL: If this tool returns 0 results, STOP searching for this entity because the results were 0 - it is not in Infoscience. Do not try variations or search again.** Args: @@ -693,7 +713,7 @@ async def search_infoscience_authors_tool(name: str, max_results: int = 10) -> s IMPORTANT: This tool caches results - don't search for the same person multiple times! Be strategic and avoid redundant searches. - + **CRITICAL: If this tool returns 0 results, STOP searching for this entity because the results were 0 - it is not in Infoscience. Do not try variations or search again.** Args: @@ -740,7 +760,7 @@ async def search_infoscience_labs_tool(name: str, max_results: int = 10) -> str: IMPORTANT: This tool caches results - don't search for the same lab multiple times! If a lab isn't found, it may not be in Infoscience or has a different name - don't keep trying! - + **CRITICAL: If this tool returns 0 results, STOP searching for this entity because the results were 0 - it is not in Infoscience. Do not try variations or search again.** Args: @@ -876,4 +896,3 @@ def normalize_infoscience_lab_url(url_or_uuid: str) -> Optional[str]: from ..agents.validation_utils import normalize_infoscience_url return normalize_infoscience_url(url_or_uuid, "orgunit") - diff --git a/src/context/repository.py b/src/context/repository.py index cef4b61..d3bc013 100644 --- a/src/context/repository.py +++ b/src/context/repository.py @@ -10,6 +10,7 @@ import logging import os import re +import shutil import tempfile from datetime import datetime from pathlib import Path @@ -65,6 +66,9 @@ "Dockerfile", ".dockerignore", ".gitignore", + # Citation and attribution metadata files + "CITATION.cff", + "codemeta.json", } # Additional important documentation and metadata files (without extensions) @@ -81,6 +85,9 @@ "ACKNOWLEDGMENTS", "ACKNOWLEDGEMENTS", "THANKS", + # Additional attribution files + "ATTRIBUTION", + "ATTRIBUTIONS", } # All relevant extensions combined @@ -91,6 +98,11 @@ | RICH_CONTENT_EXTENSIONS ) +# Extraction mode constants +EXTRACTION_MODE_README_ONLY = "readme_only" +EXTRACTION_MODE_MARKDOWN_ONLY = "markdown_only" +EXTRACTION_MODE_ALL = "all" + async def clone_repo( repo_url: str, @@ -115,6 +127,14 @@ async def clone_repo( try: logger.info(f"Clone attempt {attempt}/{max_retries}") + # Clean up any partial clone from previous attempt + if attempt > 1 and os.path.exists(temp_dir): + try: + shutil.rmtree(temp_dir) + logger.debug("Cleaned up partial clone from previous attempt") + except Exception as e: + logger.warning(f"Failed to clean up partial clone: {e}") + process = await asyncio.create_subprocess_exec( "git", "clone", @@ -185,6 +205,7 @@ async def clone_repo( "unexpected disconnect", "Connection timed out", "Failed to connect", + "Recv failure", # curl error: "curl 56 Recv failure: Connection reset by peer" ] if any(error in stderr_text for error in retryable_errors): @@ -236,13 +257,18 @@ def is_binary_file(filepath: str) -> bool: return True -def is_relevant_file(filepath: str, filename: str) -> bool: +def is_relevant_file( + filepath: str, + filename: str, + extraction_mode: str = EXTRACTION_MODE_README_ONLY, +) -> bool: """ - Check if a file is relevant for extraction. + Check if a file is relevant for extraction based on the extraction mode. Args: filepath: Full path to the file filename: Name of the file + extraction_mode: Extraction mode ("readme_only", "markdown_only", or "all") Returns: True if relevant, False otherwise @@ -258,7 +284,29 @@ def is_relevant_file(filepath: str, filename: str) -> bool: # Check by filename (case-insensitive for special files) lower_filename = filename.lower() upper_filename = filename.upper() + basename_upper = os.path.splitext(filename)[0].upper() + + # README-only mode: only README and AUTHORS files + if extraction_mode == EXTRACTION_MODE_README_ONLY: + # Check for README files (case-insensitive, with or without extension) + if lower_filename.startswith("readme"): + return True + # Check for AUTHORS files (case-insensitive, with or without extension) + if basename_upper == "AUTHORS" or upper_filename == "AUTHORS": + return True + return False + # Markdown-only mode: only .md files + if extraction_mode == EXTRACTION_MODE_MARKDOWN_ONLY: + _, ext = os.path.splitext(filename) + ext_lower = ext.lower() + if ext_lower == ".md": + # Additional check for binary files + if not is_binary_file(filepath): + return True + return False + + # All mode: current behavior (all existing checks) # Check for README, LICENSE, CITATION (with or without extensions) if any( lower_filename.startswith(name.lower()) or lower_filename == name.lower() @@ -275,7 +323,6 @@ def is_relevant_file(filepath: str, filename: str) -> bool: return True # Also check with common extensions for these files - basename_upper = os.path.splitext(filename)[0].upper() if basename_upper in IMPORTANT_FILENAMES: return True @@ -291,12 +338,16 @@ def is_relevant_file(filepath: str, filename: str) -> bool: return False -def walk_repository_tree(repo_dir: str) -> Tuple[List[str], str]: +def walk_repository_tree( + repo_dir: str, + extraction_mode: str = EXTRACTION_MODE_README_ONLY, +) -> Tuple[List[str], str]: """ Walk the repository directory tree and collect relevant files. Args: repo_dir: Root directory of the repository + extraction_mode: Extraction mode ("readme_only", "markdown_only", or "all") Returns: Tuple of (list of file paths, tree structure as string) @@ -317,9 +368,10 @@ def build_tree(directory: Path, prefix: str = "", is_last: bool = True): except PermissionError: return - for index, item in enumerate(items): - is_last_item = index == len(items) - 1 - + # Filter items based on extraction mode + # In restricted modes, only include relevant files/directories + filtered_items = [] + for item in items: # Skip hidden files and unwanted directories if item.name.startswith(".") and item.name not in { ".env", @@ -331,6 +383,44 @@ def build_tree(directory: Path, prefix: str = "", is_last: bool = True): if item.is_dir() and should_skip_directory(item.name): continue + # In restricted modes, skip files that aren't relevant + if item.is_file(): + if not is_relevant_file(str(item), item.name, extraction_mode): + continue + # File is relevant, add to both tree and relevant_files + relevant_files.append(str(item)) + + # For directories, we need to check if they contain any relevant files + # In restricted modes, we'll only include directories that have relevant content + if item.is_dir(): + # Check if directory contains any relevant files + if extraction_mode in { + EXTRACTION_MODE_README_ONLY, + EXTRACTION_MODE_MARKDOWN_ONLY, + }: + # In restricted modes, check if directory has relevant content + has_relevant_content = False + try: + for subitem in item.rglob("*"): + if subitem.is_file() and is_relevant_file( + str(subitem), + subitem.name, + extraction_mode, + ): + has_relevant_content = True + break + except (PermissionError, OSError): + pass + + if not has_relevant_content: + continue + + filtered_items.append(item) + + # Build tree from filtered items + for index, item in enumerate(filtered_items): + is_last_item = index == len(filtered_items) - 1 + # Tree formatting connector = "└── " if is_last_item else "├── " tree_lines.append(f"{prefix}{connector}{item.name}") @@ -338,10 +428,6 @@ def build_tree(directory: Path, prefix: str = "", is_last: bool = True): if item.is_dir(): extension = " " if is_last_item else "│ " build_tree(item, prefix + extension, is_last_item) - elif item.is_file(): - # Check if file is relevant - if is_relevant_file(str(item), item.name): - relevant_files.append(str(item)) # Build the tree tree_lines.append(f"{repo_path.name}/") @@ -530,12 +616,16 @@ def extract_r_imports(content: str) -> Set[str]: return imports -def generate_repository_markdown(repo_dir: str) -> str: +def generate_repository_markdown( + repo_dir: str, + extraction_mode: str = EXTRACTION_MODE_README_ONLY, +) -> str: """ Generate comprehensive markdown documentation of repository contents. Args: repo_dir: Root directory of the repository + extraction_mode: Extraction mode ("readme_only", "markdown_only", or "all") Returns: Markdown formatted string with repository information @@ -543,7 +633,56 @@ def generate_repository_markdown(repo_dir: str) -> str: logger.info(f"Generating repository markdown for {repo_dir}") # Walk repository and collect files - file_paths, tree_structure = walk_repository_tree(repo_dir) + file_paths, tree_structure = walk_repository_tree(repo_dir, extraction_mode) + + # Log file extraction statistics + if file_paths: + # Categorize files by type + readme_files = [] + authors_files = [] + markdown_files = [] + other_files = [] + + for filepath in file_paths: + filename = os.path.basename(filepath) + lower_filename = filename.lower() + basename, ext = os.path.splitext(filename) + + if lower_filename.startswith("readme"): + readme_files.append(filepath) + elif basename.upper() == "AUTHORS" or filename.upper() == "AUTHORS": + authors_files.append(filepath) + elif ext.lower() == ".md": + markdown_files.append(filepath) + else: + other_files.append(filepath) + + # Log summary + logger.info( + f"Extraction mode '{extraction_mode}': Found {len(file_paths)} file(s) - " + f"README: {len(readme_files)}, AUTHORS: {len(authors_files)}, " + f"Markdown: {len(markdown_files)}, Other: {len(other_files)}", + ) + + # Log file paths (up to 10 files to avoid log spam) + if readme_files: + logger.info(f"README files found: {readme_files[:10]}") + if len(readme_files) > 10: + logger.info(f"... and {len(readme_files) - 10} more README files") + if authors_files: + logger.info(f"AUTHORS files found: {authors_files}") + if markdown_files and extraction_mode == EXTRACTION_MODE_MARKDOWN_ONLY: + logger.info(f"Markdown files found: {markdown_files[:10]}") + if len(markdown_files) > 10: + logger.info(f"... and {len(markdown_files) - 10} more markdown files") + if other_files and extraction_mode == EXTRACTION_MODE_ALL: + logger.debug(f"Other files found: {other_files[:10]}") + if len(other_files) > 10: + logger.debug(f"... and {len(other_files) - 10} more files") + else: + logger.warning( + f"No files found for extraction mode '{extraction_mode}' in {repo_dir}", + ) markdown_parts = [] @@ -643,7 +782,10 @@ def generate_repository_markdown(repo_dir: str) -> str: return result -async def extract_git_authors(temp_dir: str) -> List[GitAuthor]: +async def extract_git_authors( + temp_dir: str, + anonymize_email: bool = True, +) -> List[GitAuthor]: """ Extract git authors from the cloned repository using git shortlog. Returns a list of GitAuthor objects with commit counts and first/last commit dates. @@ -654,7 +796,8 @@ async def extract_git_authors(temp_dir: str) -> List[GitAuthor]: 10 Carlos Args: - temp_dir: Directory containing the cloned repository + temp_dir: Directory containing the cloned repository. + anonymize_email: Whether to hash the email local part while keeping the domain. Returns: List of GitAuthor objects @@ -771,9 +914,17 @@ async def extract_git_authors(temp_dir: str) -> List[GitAuthor]: lastCommitDate=last_commit_date, ) - git_authors.append( - GitAuthor(name=name, email=email, commits=commits), + # Create GitAuthor (id will be computed automatically by model_validator) + git_author = GitAuthor(name=name, email=email, commits=commits) + if anonymize_email: + git_author.anonymize_email_local_part() + logger.debug( + "Created GitAuthor: %s (%s) [id: %s]", + name, + git_author.email if anonymize_email else email, + git_author.id, ) + git_authors.append(git_author) logger.info(f"Extracted {len(git_authors)} git authors from repository.") return git_authors @@ -822,6 +973,7 @@ def reduce_input_size( async def prepare_repository_context( repo_url: str, max_tokens: int = 400000, + extraction_mode: str = EXTRACTION_MODE_README_ONLY, ) -> Dict[str, Any]: """ Prepare repository context by cloning, extracting text, and getting git authors. @@ -829,6 +981,8 @@ async def prepare_repository_context( Args: repo_url: Repository URL to process max_tokens: Maximum tokens for text content + extraction_mode: Extraction mode ("readme_only", "markdown_only", or "all"). + Defaults to "readme_only" to minimize token usage. Returns: Dictionary containing: @@ -844,6 +998,11 @@ async def prepare_repository_context( "error": None, } + # Log extraction mode + logger.info( + f"Using extraction mode: {extraction_mode} for repository {repo_url}", + ) + # Clone the GitHub repository into a temporary folder with tempfile.TemporaryDirectory() as temp_dir: # Clone repository asynchronously @@ -854,7 +1013,7 @@ async def prepare_repository_context( # Generate comprehensive markdown documentation of repository try: - input_text = generate_repository_markdown(temp_dir) + input_text = generate_repository_markdown(temp_dir, extraction_mode) input_text = sanitize_special_tokens(input_text) except Exception as e: logger.error(f"Failed to generate repository markdown: {e}", exc_info=True) diff --git a/src/data_models/__init__.py b/src/data_models/__init__.py index 6006627..d0def05 100644 --- a/src/data_models/__init__.py +++ b/src/data_models/__init__.py @@ -1,12 +1,6 @@ """Data models and schemas for the application.""" # __init__.py - Clean up exports -from .academic_catalog import ( - AcademicCatalogEnrichmentResult, - AcademicCatalogRelation, - CatalogType, - EntityType, -) from .api import APIOutput from .conversion import ( convert_jsonld_to_pydantic, @@ -15,11 +9,18 @@ from .epfl_assessment import EPFLAssessmentResult, EvidenceItem from .infoscience import ( InfoscienceAuthor, - InfoscienceLab, + InfoscienceOrgUnit, InfosciencePublication, InfoscienceSearchResult, ) +from .linked_entities import ( + CatalogType, + EntityType, + linkedEntitiesEnrichmentResult, + linkedEntitiesRelation, +) from .models import ( + Affiliation, Discipline, Organization, Person, @@ -68,6 +69,7 @@ # Core models "Person", "Organization", + "Affiliation", "Discipline", "RepositoryType", "ResourceType", @@ -77,7 +79,7 @@ "Commits", "Image", "ImageKeyword", - "InfoscienceEntity", # Deprecated - use AcademicCatalogRelation + "InfoscienceEntity", # Deprecated - use linkedEntitiesRelation "FundingInformation", "FormalParameter", "ExecutableNotebook", @@ -100,14 +102,14 @@ "OrganizationAnalysisContext", "GitHubOrganizationMetadata", # Academic Catalog models - "AcademicCatalogRelation", - "AcademicCatalogEnrichmentResult", + "linkedEntitiesRelation", + "linkedEntitiesEnrichmentResult", "CatalogType", "EntityType", # Infoscience models "InfosciencePublication", "InfoscienceAuthor", - "InfoscienceLab", + "InfoscienceOrgUnit", "InfoscienceSearchResult", # API models "APIOutput", @@ -127,12 +129,12 @@ ] # Rebuild models after all imports to resolve forward references -# This must happen after AcademicCatalogRelation is imported +# This must happen after linkedEntitiesRelation is imported import sys from typing import Any, Dict, List, Optional, Union _module = sys.modules[Person.__module__] -_module.AcademicCatalogRelation = AcademicCatalogRelation +_module.linkedEntitiesRelation = linkedEntitiesRelation _module.InfoscienceEntity = ( InfoscienceEntity # Keep for backward compatibility during migration ) @@ -145,7 +147,7 @@ Person.model_rebuild() Organization.model_rebuild() # Clean up namespace -delattr(_module, "AcademicCatalogRelation") +delattr(_module, "linkedEntitiesRelation") delattr(_module, "InfoscienceEntity") delattr(_module, "List") delattr(_module, "Dict") @@ -153,108 +155,108 @@ delattr(_module, "Union") delattr(_module, "Any") -# Rebuild SoftwareSourceCode to resolve AcademicCatalogRelation forward reference +# Rebuild SoftwareSourceCode to resolve linkedEntitiesRelation forward reference _repo_module = sys.modules[SoftwareSourceCode.__module__] -_repo_module.AcademicCatalogRelation = AcademicCatalogRelation +_repo_module.linkedEntitiesRelation = linkedEntitiesRelation _repo_module.List = List _repo_module.Dict = Dict _repo_module.Optional = Optional _repo_module.Union = Union _repo_module.Any = Any SoftwareSourceCode.model_rebuild() -delattr(_repo_module, "AcademicCatalogRelation") +delattr(_repo_module, "linkedEntitiesRelation") delattr(_repo_module, "List") delattr(_repo_module, "Dict") delattr(_repo_module, "Optional") delattr(_repo_module, "Union") delattr(_repo_module, "Any") -# Rebuild EnrichedAuthor to resolve AcademicCatalogRelation forward reference +# Rebuild EnrichedAuthor to resolve linkedEntitiesRelation forward reference _user_module = sys.modules[EnrichedAuthor.__module__] -_user_module.AcademicCatalogRelation = AcademicCatalogRelation +_user_module.linkedEntitiesRelation = linkedEntitiesRelation _user_module.List = List _user_module.Dict = Dict _user_module.Optional = Optional _user_module.Union = Union _user_module.Any = Any EnrichedAuthor.model_rebuild() -delattr(_user_module, "AcademicCatalogRelation") +delattr(_user_module, "linkedEntitiesRelation") delattr(_user_module, "List") delattr(_user_module, "Dict") delattr(_user_module, "Optional") delattr(_user_module, "Union") delattr(_user_module, "Any") -# Rebuild GitHubUser to resolve AcademicCatalogRelation forward reference +# Rebuild GitHubUser to resolve linkedEntitiesRelation forward reference _githubuser_module = sys.modules[GitHubUser.__module__] -_githubuser_module.AcademicCatalogRelation = AcademicCatalogRelation +_githubuser_module.linkedEntitiesRelation = linkedEntitiesRelation _githubuser_module.List = List _githubuser_module.Dict = Dict _githubuser_module.Optional = Optional _githubuser_module.Union = Union _githubuser_module.Any = Any GitHubUser.model_rebuild() -delattr(_githubuser_module, "AcademicCatalogRelation") +delattr(_githubuser_module, "linkedEntitiesRelation") delattr(_githubuser_module, "List") delattr(_githubuser_module, "Dict") delattr(_githubuser_module, "Optional") delattr(_githubuser_module, "Union") delattr(_githubuser_module, "Any") -# Rebuild GitHubOrganization to resolve AcademicCatalogRelation forward reference +# Rebuild GitHubOrganization to resolve linkedEntitiesRelation forward reference _org_module = sys.modules[GitHubOrganization.__module__] -_org_module.AcademicCatalogRelation = AcademicCatalogRelation +_org_module.linkedEntitiesRelation = linkedEntitiesRelation _org_module.List = List _org_module.Dict = Dict _org_module.Optional = Optional _org_module.Union = Union _org_module.Any = Any GitHubOrganization.model_rebuild() -delattr(_org_module, "AcademicCatalogRelation") +delattr(_org_module, "linkedEntitiesRelation") delattr(_org_module, "List") delattr(_org_module, "Dict") delattr(_org_module, "Optional") delattr(_org_module, "Union") delattr(_org_module, "Any") -# Rebuild OrganizationLLMAnalysisResult to resolve AcademicCatalogRelation forward reference +# Rebuild OrganizationLLMAnalysisResult to resolve linkedEntitiesRelation forward reference _orgllm_module = sys.modules[OrganizationLLMAnalysisResult.__module__] -_orgllm_module.AcademicCatalogRelation = AcademicCatalogRelation +_orgllm_module.linkedEntitiesRelation = linkedEntitiesRelation _orgllm_module.List = List _orgllm_module.Dict = Dict _orgllm_module.Optional = Optional _orgllm_module.Union = Union _orgllm_module.Any = Any OrganizationLLMAnalysisResult.model_rebuild() -delattr(_orgllm_module, "AcademicCatalogRelation") +delattr(_orgllm_module, "linkedEntitiesRelation") delattr(_orgllm_module, "List") delattr(_orgllm_module, "Dict") delattr(_orgllm_module, "Optional") delattr(_orgllm_module, "Union") delattr(_orgllm_module, "Any") -# Rebuild AcademicCatalogRelation to resolve List typing import -_catalog_relation_module = sys.modules[AcademicCatalogRelation.__module__] +# Rebuild linkedEntitiesRelation to resolve List typing import +_catalog_relation_module = sys.modules[linkedEntitiesRelation.__module__] _catalog_relation_module.List = List _catalog_relation_module.Dict = Dict _catalog_relation_module.Optional = Optional _catalog_relation_module.Union = Union _catalog_relation_module.Any = Any -AcademicCatalogRelation.model_rebuild() +linkedEntitiesRelation.model_rebuild() delattr(_catalog_relation_module, "List") delattr(_catalog_relation_module, "Dict") delattr(_catalog_relation_module, "Optional") delattr(_catalog_relation_module, "Union") delattr(_catalog_relation_module, "Any") -# Rebuild AcademicCatalogEnrichmentResult to resolve List and other typing imports -_catalog_module = sys.modules[AcademicCatalogEnrichmentResult.__module__] +# Rebuild linkedEntitiesEnrichmentResult to resolve List and other typing imports +_catalog_module = sys.modules[linkedEntitiesEnrichmentResult.__module__] _catalog_module.List = List _catalog_module.Dict = Dict _catalog_module.Optional = Optional _catalog_module.Union = Union _catalog_module.Any = Any -AcademicCatalogEnrichmentResult.model_rebuild() +linkedEntitiesEnrichmentResult.model_rebuild() delattr(_catalog_module, "List") delattr(_catalog_module, "Dict") delattr(_catalog_module, "Optional") diff --git a/src/data_models/api.py b/src/data_models/api.py index bfef7b7..b56c7f1 100644 --- a/src/data_models/api.py +++ b/src/data_models/api.py @@ -7,8 +7,6 @@ from pydantic import ( BaseModel, HttpUrl, - field_validator, - model_serializer, ) from .models import ResourceType @@ -18,12 +16,23 @@ class APIStats(BaseModel): + """ + API usage statistics. + + Note: For OpenAI-compatible endpoints that don't report usage information + (e.g., custom endpoints, OSS models), the agent_input_tokens and agent_output_tokens + fields will be populated from tiktoken estimates instead of API-reported values. + This ensures meaningful token counts are always available even when the API + doesn't provide usage data. + """ + # Official API-reported token counts agent_input_tokens: int = None agent_output_tokens: int = None total_tokens: int = None # Tokenizer-based estimates (complementary/fallback) + # When API returns 0, these estimates are used as the primary metric estimated_input_tokens: int = None estimated_output_tokens: int = None estimated_total_tokens: int = None @@ -78,25 +87,3 @@ class APIOutput(BaseModel): Any, ] = None stats: APIStats = None - - @field_validator("output", mode="before") - @classmethod - def preserve_dict_output(cls, v): - """Preserve dict/list output as-is without converting to Pydantic models.""" - # If it's already a dict or list (e.g., JSON-LD), don't try to convert it - if isinstance(v, (dict, list)): - return v - # Otherwise, let Pydantic handle it normally - return v - - @model_serializer(mode="wrap") - def serialize_model(self, serializer): - """Custom serializer to preserve dict/list in output field.""" - # Serialize the model normally - data = serializer(self) - - # If output is a dict or list, keep it as-is (don't convert to model) - if isinstance(self.output, (dict, list)): - data["output"] = self.output - - return data diff --git a/src/data_models/conversion.py b/src/data_models/conversion.py index fc59d7f..f26bdea 100644 --- a/src/data_models/conversion.py +++ b/src/data_models/conversion.py @@ -4,10 +4,21 @@ from datetime import date, datetime from enum import Enum -from typing import Any, Dict, Optional, Union, get_args, get_origin +from typing import ( + Any, + Dict, + Literal, + Optional, + Tuple, + Type, + Union, + get_args, + get_origin, +) +from typing import Dict as DictType from typing import List as ListType -from pydantic import BaseModel, HttpUrl +from pydantic import BaseModel, Field, HttpUrl, create_model from .models import ( Organization, @@ -33,63 +44,158 @@ # A dictionary to map JSON-LD property URIs to functions that can convert them. # This provides a clean, declarative way to define the conversion process. +# Updated to align with PULSE ontology JSONLD_TO_PYDANTIC_MAPPING = { # Schema.org properties "http://schema.org/name": "name", + "schema:name": "name", "http://schema.org/description": "description", + "schema:description": "description", "http://schema.org/url": "url", + "schema:url": "url", "http://schema.org/identifier": "identifier", + "schema:identifier": "identifier", "http://schema.org/dateCreated": "dateCreated", + "schema:dateCreated": "dateCreated", "http://schema.org/datePublished": "datePublished", + "schema:datePublished": "datePublished", "http://schema.org/license": "license", + "schema:license": "license", "http://schema.org/author": "author", + "schema:author": "author", "http://schema.org/codeRepository": "codeRepository", + "schema:codeRepository": "codeRepository", "http://schema.org/programmingLanguage": "programmingLanguage", + "schema:programmingLanguage": "programmingLanguage", "http://schema.org/applicationCategory": "applicationCategory", + "schema:applicationCategory": "applicationCategory", "http://schema.org/featureList": "featureList", + "schema:featureList": "featureList", "http://schema.org/image": "image", + "schema:image": "image", "http://schema.org/isAccessibleForFree": "isAccessibleForFree", + "schema:isAccessibleForFree": "isAccessibleForFree", "http://schema.org/isBasedOn": "isBasedOn", + "schema:isBasedOn": "isBasedOn", "http://schema.org/operatingSystem": "operatingSystem", + "schema:operatingSystem": "operatingSystem", "http://schema.org/softwareRequirements": "softwareRequirements", + "schema:softwareRequirements": "softwareRequirements", "http://schema.org/processorRequirements": "processorRequirements", + "schema:processorRequirements": "processorRequirements", "http://schema.org/memoryRequirements": "memoryRequirements", + "schema:memoryRequirements": "memoryRequirements", "http://schema.org/supportingData": "supportingData", + "schema:supportingData": "supportingData", "http://schema.org/conditionsOfAccess": "conditionsOfAccess", + "schema:conditionsOfAccess": "conditionsOfAccess", "http://schema.org/citation": "citation", + "schema:citation": "citation", "http://schema.org/affiliation": "affiliation", + "schema:affiliation": "affiliation", "http://schema.org/legalName": "legalName", + "schema:legalName": "legalName", "http://schema.org/encodingFormat": "encodingFormat", + "schema:encodingFormat": "encodingFormat", "http://schema.org/defaultValue": "defaultValue", + "schema:defaultValue": "defaultValue", "http://schema.org/valueRequired": "valueRequired", + "schema:valueRequired": "valueRequired", "http://schema.org/measurementTechnique": "measurementTechnique", + "schema:measurementTechnique": "measurementTechnique", "http://schema.org/variableMeasured": "variableMeasured", + "schema:variableMeasured": "variableMeasured", "http://schema.org/contentUrl": "contentUrl", + "schema:contentUrl": "contentUrl", "http://schema.org/softwareVersion": "softwareVersion", + "schema:softwareVersion": "softwareVersion", + "http://schema.org/email": "email", + "schema:email": "email", + "http://schema.org/username": "username", + "schema:username": "username", + "http://schema.org/memberOf": "memberOf", + "schema:memberOf": "memberOf", + "http://schema.org/keywords": "keywords", + "schema:keywords": "keywords", + "http://schema.org/abstract": "abstract", + "schema:abstract": "abstract", + "http://schema.org/parentOrganization": "parentOrganization", + "schema:parentOrganization": "parentOrganization", + "http://schema.org/knowsAbout": "knowsAbout", + "schema:knowsAbout": "knowsAbout", # SD ontology properties "https://w3id.org/okn/o/sd#hasDocumentation": "hasDocumentation", + "sd:hasDocumentation": "hasDocumentation", "https://w3id.org/okn/o/sd#hasExecutableInstructions": "hasExecutableInstructions", + "sd:hasExecutableInstructions": "hasExecutableInstructions", "https://w3id.org/okn/o/sd#hasAcknowledgements": "hasAcknowledgements", + "sd:hasAcknowledgements": "hasAcknowledgements", "https://w3id.org/okn/o/sd#readme": "readme", + "sd:readme": "readme", "https://w3id.org/okn/o/sd#hasFunding": "hasFunding", + "sd:hasFunding": "hasFunding", "https://w3id.org/okn/o/sd#hasSoftwareImage": "hasSoftwareImage", + "sd:hasSoftwareImage": "hasSoftwareImage", "https://w3id.org/okn/o/sd#hasFormat": "hasFormat", + "sd:hasFormat": "hasFormat", "https://w3id.org/okn/o/sd#hasDimensionality": "hasDimensionality", + "sd:hasDimensionality": "hasDimensionality", "https://w3id.org/okn/o/sd#availableInRegistry": "availableInRegistry", + "sd:availableInRegistry": "availableInRegistry", "https://w3id.org/okn/o/sd#fundingGrant": "fundingGrant", + "sd:fundingGrant": "fundingGrant", "https://w3id.org/okn/o/sd#fundingSource": "fundingSource", - # Imaging Plaza specific properties - "https://imaging-plaza.epfl.ch/ontology#imagingModality": "imagingModality", - "https://imaging-plaza.epfl.ch/ontology#isPluginModuleOf": "isPluginModuleOf", - "https://imaging-plaza.epfl.ch/ontology#relatedToOrganization": "relatedToOrganization", - "https://imaging-plaza.epfl.ch/ontology#requiresGPU": "requiresGPU", - "https://imaging-plaza.epfl.ch/ontology#hasExecutableNotebook": "hasExecutableNotebook", + "sd:fundingSource": "fundingSource", + # PULSE ontology properties (updated from imaging-plaza) + "https://open-pulse.epfl.ch/ontology#imagingModality": "imagingModality", + "pulse:imagingModality": "imagingModality", + "https://open-pulse.epfl.ch/ontology#isPluginModuleOf": "isPluginModuleOf", + "pulse:isPluginModuleOf": "isPluginModuleOf", + "https://open-pulse.epfl.ch/ontology#relatedToOrganization": "relatedToOrganization", + "pulse:relatedToOrganization": "relatedToOrganization", + "https://open-pulse.epfl.ch/ontology#requiresGPU": "requiresGPU", + "pulse:requiresGPU": "requiresGPU", + "https://open-pulse.epfl.ch/ontology#hasExecutableNotebook": "hasExecutableNotebook", + "pulse:hasExecutableNotebook": "hasExecutableNotebook", + "https://open-pulse.epfl.ch/ontology#gitAuthors": "gitAuthors", + "pulse:gitAuthors": "gitAuthors", + "https://open-pulse.epfl.ch/ontology#commits": "commits", + "pulse:commits": "commits", + "https://open-pulse.epfl.ch/ontology#discipline": "discipline", + "pulse:discipline": "discipline", + "https://open-pulse.epfl.ch/ontology#repositoryType": "repositoryType", + "pulse:repositoryType": "repositoryType", + "https://open-pulse.epfl.ch/ontology#username": "username", + "pulse:username": "username", + "https://open-pulse.epfl.ch/ontology#hasRepository": "hasRepository", + "pulse:hasRepository": "hasRepository", + "https://open-pulse.epfl.ch/ontology#hasAcademicCatalogRelation": "hasAcademicCatalogRelation", + "pulse:hasAcademicCatalogRelation": "hasAcademicCatalogRelation", + "https://open-pulse.epfl.ch/ontology#catalogType": "catalogType", + "pulse:catalogType": "catalogType", + "https://open-pulse.epfl.ch/ontology#entityType": "entityType", + "pulse:entityType": "entityType", + "https://open-pulse.epfl.ch/ontology#hasCatalogEntity": "hasCatalogEntity", + "pulse:hasCatalogEntity": "hasCatalogEntity", + "https://open-pulse.epfl.ch/ontology#confidence": "confidence", + "pulse:confidence": "confidence", + "https://open-pulse.epfl.ch/ontology#justification": "justification", + "pulse:justification": "justification", + "https://open-pulse.epfl.ch/ontology#matchedOn": "matchedOn", + "pulse:matchedOn": "matchedOn", + "https://open-pulse.epfl.ch/ontology#uuid": "uuid", + "pulse:uuid": "uuid", + "https://open-pulse.epfl.ch/ontology#email": "email", + "pulse:email": "email", + "https://open-pulse.epfl.ch/ontology#profileUrl": "profileUrl", + "pulse:profileUrl": "profileUrl", # MD4I properties "http://w3id.org/nfdi4ing/metadata4ing#orcid": "orcid", + "md4i:orcid": "orcid", + "http://w3id.org/nfdi4ing/metadata4ing#orcidId": "orcid", + "md4i:orcidId": "orcid", "http://w3id.org/nfdi4ing/metadata4ing#hasRorId": "hasRorId", - # Git metadata - "https://imaging-plaza.epfl.ch/ontology#gitAuthors": "gitAuthors", - "https://imaging-plaza.epfl.ch/ontology#commits": "commits", + "md4i:hasRorId": "hasRorId", } @@ -133,9 +239,8 @@ def _convert_entity(entity: Dict, all_entities: Dict) -> Optional[BaseModel]: email_extracted = _get_value(email_value) if email_extracted: person_data["email"] = email_extracted - - # All other fields (gitAuthorIds, affiliations, currentAffiliation, - # affiliationHistory, contributionSummary, biography, infoscienceEntity) + + # All other fields (affiliationHistory, linkedEntities, etc.) # will use their default values as defined in the Person model return Person(**person_data) @@ -157,14 +262,20 @@ def _convert_entity(entity: Dict, all_entities: Dict) -> Optional[BaseModel]: ) if "https://w3id.org/okn/o/sd#FundingInformation" in entity_types: source_ref = _get_value(entity.get("https://w3id.org/okn/o/sd#fundingSource")) + funding_source = Organization( + type="Organization", + legalName="Unknown", + ) # Default + if source_ref and source_ref in all_entities: + converted = _convert_entity(all_entities[source_ref], all_entities) + if isinstance(converted, Organization): + funding_source = converted return FundingInformation( identifier=_get_value(entity.get("http://schema.org/identifier")), fundingGrant=_get_value( entity.get("https://w3id.org/okn/o/sd#fundingGrant"), ), - fundingSource=_convert_entity(all_entities[source_ref], all_entities) - if source_ref in all_entities - else None, + fundingSource=funding_source, ) if "https://w3id.org/okn/o/sd#FormalParameter" in entity_types: return FormalParameter( @@ -255,10 +366,10 @@ def _convert_entity(entity: Dict, all_entities: Dict) -> Optional[BaseModel]: else: # Handle single values data[pydantic_key] = _get_value(value) + return SoftwareSourceCode(**data) return None - def convert_jsonld_to_pydantic( jsonld_graph: ListType[Dict[str, Any]], ) -> Optional[SoftwareSourceCode]: @@ -296,29 +407,64 @@ def convert_jsonld_to_pydantic( PYDANTIC_TO_ZOD_MAPPING = { "Person": { + "type": "@type", "name": "schema:name", - "orcid": "md4i:orcid", - "affiliation": "schema:affiliation", + "emails": "schema:email", + "githubId": "schema:username", + "orcid": "md4i:orcidId", + "affiliations": "schema:affiliation", + "affiliationHistory": "pulse:affiliationHistory", + "source": "pulse:source", + "linkedEntities": "pulse:linkedEntities", }, "Organization": { + "type": "@type", "legalName": "schema:legalName", "hasRorId": "md4i:hasRorId", - "alternateNames": "schema:alternateName", "organizationType": "schema:additionalType", "parentOrganization": "schema:parentOrganization", "country": "schema:addressCountry", "website": "schema:url", - "attributionConfidence": "imag:attributionConfidence", + "attributionConfidence": "pulse:confidence", + "academicCatalogRelations": "pulse:hasAcademicCatalogRelation", + }, + "GitHubOrganization": { + "name": "schema:name", + "organizationType": "schema:additionalType", + "description": "schema:description", + "discipline": "pulse:discipline", + "disciplineJustification": "pulse:justification", + "relatedToEPFL": "pulse:relatedToEPFL", + "relatedToEPFLJustification": "pulse:justification", + "relatedToEPFLConfidence": "pulse:confidence", + "academicCatalogRelations": "pulse:hasAcademicCatalogRelation", + "githubOrganizationMetadata": "pulse:metadata", + }, + "GitHubUser": { + "name": "schema:name", + "fullname": "schema:name", + "githubHandle": "schema:username", + "githubUserMetadata": "pulse:metadata", + "relatedToOrganization": "pulse:relatedToOrganization", + "relatedToOrganizationJustification": "pulse:justification", + "discipline": "pulse:discipline", + "disciplineJustification": "pulse:justification", + "position": "schema:jobTitle", + "positionJustification": "pulse:justification", + "relatedToEPFL": "pulse:relatedToEPFL", + "relatedToEPFLJustification": "pulse:justification", + "relatedToEPFLConfidence": "pulse:confidence", + "academicCatalogRelations": "pulse:hasAcademicCatalogRelation", }, "Commits": { - "total": "imag:totalCommits", - "firstCommitDate": "imag:firstCommitDate", - "lastCommitDate": "imag:lastCommitDate", + "total": "pulse:totalCommits", + "firstCommitDate": "pulse:firstCommitDate", + "lastCommitDate": "pulse:lastCommitDate", }, "GitAuthor": { "name": "schema:name", - "email": "schema:email", - "commits": "imag:commits", + "email": "pulse:email", + "commits": "pulse:commits", }, "FundingInformation": { "identifier": "schema:identifier", @@ -359,8 +505,57 @@ def convert_jsonld_to_pydantic( "InfoscienceEntity": { "name": "schema:name", "url": "schema:url", - "confidence": "imag:confidence", - "justification": "imag:justification", + "confidence": "pulse:confidence", + "justification": "pulse:justification", + }, + "AcademicCatalogRelation": { + "catalogType": "pulse:catalogType", + "entityType": "pulse:entityType", + "entity": "pulse:hasCatalogEntity", + "confidence": "pulse:confidence", + "justification": "pulse:justification", + "matchedOn": "pulse:matchedOn", + }, + "CatalogEntity": { + "uuid": "pulse:uuid", + "name": "schema:name", + "email": "pulse:email", + "orcid": "md4i:orcidId", + "affiliation": "schema:affiliation", + "profileUrl": "pulse:profileUrl", + }, + "InfosciencePublication": { + "type": "@type", + "uuid": "pulse:uuid", + "title": "schema:name", + "authors": "schema:author", + "abstract": "schema:abstract", + "doi": "schema:identifier", + "publication_date": "schema:datePublished", + "publication_type": "schema:additionalType", + "url": "schema:url", + "repository_url": "schema:codeRepository", + "lab": "schema:affiliation", + "subjects": "schema:keywords", + }, + "InfoscienceAuthor": { + "type": "@type", + "uuid": "pulse:uuid", + "name": "schema:name", + "email": "pulse:email", + "orcid": "md4i:orcidId", + "affiliation": "schema:affiliation", + "profile_url": "pulse:profileUrl", + }, + "InfoscienceLab": { + "type": "@type", + "uuid": "pulse:uuid", + "name": "schema:name", + "description": "schema:description", + "url": "schema:url", + "parent_organization": "schema:parentOrganization", + "website": "schema:url", + "research_areas": "schema:knowsAbout", }, "SoftwareSourceCode": { "name": "schema:name", @@ -375,42 +570,42 @@ def convert_jsonld_to_pydantic( "image": "schema:image", "isAccessibleForFree": "schema:isAccessibleForFree", "isBasedOn": "schema:isBasedOn", - "isPluginModuleOf": "imag:isPluginModuleOf", + "isPluginModuleOf": "pulse:isPluginModuleOf", "license": "schema:license", "author": "schema:author", - "relatedToOrganization": "imag:relatedToOrganization", + "relatedToOrganizations": "pulse:relatedToOrganization", "operatingSystem": "schema:operatingSystem", "programmingLanguage": "schema:programmingLanguage", "softwareRequirements": "schema:softwareRequirements", "processorRequirements": "schema:processorRequirements", "memoryRequirements": "schema:memoryRequirements", - "requiresGPU": "imag:requiresGPU", + "requiresGPU": "pulse:requiresGPU", "supportingData": "schema:supportingData", "url": "schema:url", "identifier": "schema:identifier", "hasAcknowledgements": "sd:hasAcknowledgements", "hasDocumentation": "sd:hasDocumentation", "hasExecutableInstructions": "sd:hasExecutableInstructions", - "hasExecutableNotebook": "imag:hasExecutableNotebook", + "hasExecutableNotebook": "pulse:hasExecutableNotebook", "readme": "sd:readme", "hasFunding": "sd:hasFunding", "hasSoftwareImage": "sd:hasSoftwareImage", - "imagingModality": "imag:imagingModality", - "gitAuthors": "imag:gitAuthors", - "relatedToOrganizations": "imag:relatedToOrganizations", - "relatedToOrganizationJustification": "imag:relatedToOrganizationJustification", - "repositoryType": "imag:repositoryType", - "repositoryTypeJustification": "imag:repositoryTypeJustification", - "relatedToEPFL": "imag:relatedToEPFL", - "relatedToEPFLConfidence": "imag:relatedToEPFLConfidence", - "relatedToEPFLJustification": "imag:relatedToEPFLJustification", - "infoscienceEntities": "imag:infoscienceEntities", - "relatedDatasets": "imag:relatedDatasets", - "relatedPublications": "imag:relatedPublications", - "relatedModels": "imag:relatedModels", - "relatedAPIs": "imag:relatedAPIs", - "discipline": "imag:discipline", - "disciplineJustification": "imag:disciplineJustification", + "imagingModality": "pulse:imagingModality", + "gitAuthors": "pulse:gitAuthors", + "relatedToOrganizationJustification": "pulse:justification", + "repositoryType": "pulse:repositoryType", + "repositoryTypeJustification": "pulse:justification", + "relatedToEPFL": "pulse:relatedToEPFL", + "relatedToEPFLConfidence": "pulse:confidence", + "relatedToEPFLJustification": "pulse:justification", + "academicCatalogRelations": "pulse:hasAcademicCatalogRelation", + "relatedDatasets": "pulse:relatedDatasets", + "relatedPublications": "pulse:relatedPublications", + "relatedModels": "pulse:relatedModels", + "relatedAPIs": "pulse:relatedAPIs", + "discipline": "pulse:discipline", + "disciplineJustification": "pulse:justification", + "linkedEntities": "pulse:linkedEntities", }, } @@ -481,15 +676,64 @@ def convert_pydantic_to_jsonld( Returns: A dictionary or list representing the JSON-LD graph """ - - # Define namespace prefixes for the @context context = { "schema": "http://schema.org/", "sd": "https://w3id.org/okn/o/sd#", - "imag": "https://imaging-plaza.epfl.ch/ontology/", - "md4i": "https://w3id.org/md4i/", + "pulse": "https://open-pulse.epfl.ch/ontology#", + "md4i": "http://w3id.org/nfdi4ing/metadata4ing#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "owl": "http://www.w3.org/2002/07/owl#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "dcterms": "http://purl.org/dc/terms/", + "wd": "http://www.wikidata.org/entity/", } + # Helper function to generate IRI for a Person + def _generate_person_iri(person_obj: Any) -> Optional[str]: + """Generate a stable IRI for a Person based on their identifiers.""" + # Priority: explicit id > githubId > ORCID > email + # New JSON structure has explicit 'id' field + if hasattr(person_obj, "id") and person_obj.id: + person_id = str(person_obj.id) + if person_id.startswith("http"): + return person_id + + # Fallback to githubId (new structure) + if hasattr(person_obj, "githubId") and person_obj.githubId: + return f"https://github.com/{person_obj.githubId}" + + if hasattr(person_obj, "orcid") and person_obj.orcid: + orcid = str(person_obj.orcid) + if orcid.startswith("http"): + return orcid + return f"https://orcid.org/{orcid}" + # New structure may include emails array + if hasattr(person_obj, "emails") and person_obj.emails: + return f"mailto:{person_obj.emails[0]}" + + return None + + # Helper to make any value JSON-serializable + def _make_json_serializable(obj: Any) -> Any: + """Recursively convert objects to JSON-serializable types.""" + if obj is None or isinstance(obj, (str, int, float, bool)): + return obj + if isinstance(obj, HttpUrl): + return str(obj) + if isinstance(obj, date): + return obj.isoformat() + if isinstance(obj, Enum): + return obj.value + if isinstance(obj, dict): + return {k: _make_json_serializable(v) for k, v in obj.items()} + if isinstance(obj, (list, tuple)): + return [_make_json_serializable(item) for item in obj] + if isinstance(obj, BaseModel): + return obj.model_dump(exclude_unset=True, exclude_none=True, mode='json') + # Fallback for other types + return str(obj) + # Helper function to convert a single entity def _convert_entity_to_jsonld( obj: Any, @@ -506,6 +750,9 @@ def _convert_entity_to_jsonld( return {"@value": obj.isoformat()} if isinstance(obj, Enum): return {"@value": obj.value} + if isinstance(obj, dict): + # Recursively ensure all values in dict are JSON-serializable + return _make_json_serializable(obj) # Return primitives as-is (strings, numbers, booleans) return obj @@ -525,22 +772,53 @@ def _convert_entity_to_jsonld( jsonld_entity["@id"] = entity_id elif base_url and model_name == "SoftwareSourceCode": jsonld_entity["@id"] = base_url + elif ( + model_name == "GitHubUser" + and hasattr(obj, "githubUserMetadata") + and obj.githubUserMetadata + ): + # Use html_url from githubUserMetadata for GitHubUser + jsonld_entity["@id"] = obj.githubUserMetadata.html_url + elif ( + model_name == "GitHubOrganization" + and hasattr(obj, "githubOrganizationMetadata") + and obj.githubOrganizationMetadata + ): + # Use html_url from githubOrganizationMetadata for GitHubOrganization + jsonld_entity["@id"] = obj.githubOrganizationMetadata.html_url + elif model_name == "Person": + # Generate IRI for Person based on their identifiers + person_iri = _generate_person_iri(obj) + if person_iri: + jsonld_entity["@id"] = person_iri + elif model_name == "GitHubUser" and base_url: + # Fallback to base_url if provided for users + jsonld_entity["@id"] = base_url + elif model_name == "GitHubOrganization" and base_url: + # Fallback to base_url if provided for organizations + jsonld_entity["@id"] = base_url - # Add @type based on model name type_mapping = { - "SoftwareSourceCode": "http://schema.org/SoftwareSourceCode", - "Person": "http://schema.org/Person", - "Organization": "http://schema.org/Organization", - "GitHubUser": "http://schema.org/Person", - "GitHubOrganization": "http://schema.org/Organization", - "DataFeed": "http://schema.org/DataFeed", - "FormalParameter": "http://schema.org/PropertyValue", - "ExecutableNotebook": "http://schema.org/SoftwareApplication", - "SoftwareImage": "http://schema.org/SoftwareApplication", - "Image": "http://schema.org/ImageObject", - "FundingInformation": "http://schema.org/Grant", - "GitAuthor": "http://schema.org/Person", - "InfoscienceEntity": "http://schema.org/Thing", + "SoftwareSourceCode": "schema:SoftwareSourceCode", + "Person": "schema:Person", + "Organization": "schema:Organization", + "GitHubOrganization": "schema:GitHubOrganization", + "GitHubUser": "schema:Person", + "DataFeed": "schema:DataFeed", + "FormalParameter": "schema:PropertyValue", + "ExecutableNotebook": "schema:SoftwareApplication", + "SoftwareImage": "schema:SoftwareApplication", + "Image": "schema:ImageObject", + "FundingInformation": "schema:Grant", + "GitAuthor": "schema:Person", + "InfoscienceEntity": "schema:Thing", + "AcademicCatalogRelation": "pulse:AcademicCatalogRelation", + "CatalogEntity": "pulse:CatalogEntity", + "InfosciencePublication": "schema:ScholarlyArticle", + "InfoscienceAuthor": "schema:Person", + "InfoscienceLab": "schema:Organization", + "Discipline": "pulse:DisciplineEnumeration", + "RepositoryType": "pulse:RepositoryTypeEnumeration", } if model_name in type_mapping: @@ -554,31 +832,269 @@ def _convert_entity_to_jsonld( if pydantic_key not in key_map: continue + # Skip the 'type' field for models - we handle @type via type_mapping + if pydantic_key == "type": + continue + jsonld_key = key_map[pydantic_key] - # Handle lists - if isinstance(value, list): - jsonld_values = [] + # Special handling for author field - just create IRI references + if pydantic_key == "author" and isinstance(value, list): + author_refs = [] + for item in value: + # Case 1: Pydantic Person model -> use IRI generator + if ( + isinstance(item, BaseModel) + and item.__class__.__name__ == "Person" + ): + person_iri = _generate_person_iri(item) + if person_iri: + author_refs.append({"@id": person_iri}) + + # Case 2: Raw dict (newer JSON) - prefer explicit 'id' field + elif isinstance(item, dict): + person_id = item.get("id") or item.get("url") or None + if person_id: + author_refs.append({"@id": person_id}) + + # Fallback: primitives or other types - try converting + else: + converted = _convert_entity_to_jsonld(item) + if converted is not None: + # If converted is a string/IRI, keep as author ref + if isinstance(converted, str) and ( + converted.startswith("http://") or converted.startswith("https://") + ): + author_refs.append({"@id": converted}) + else: + author_refs.append(converted) + + if author_refs: + jsonld_entity["schema:author"] = author_refs + + continue # Skip the normal list handling below + + # Special handling for linkedEntities - preserve linkedEntities and extract DOIs as citations + if pydantic_key == "linkedEntities" and isinstance(value, list): + linked_entities_jsonld = [] + citation_urls = [] + + for item in value: + # Item may be a Pydantic model or a raw dict if isinstance(item, BaseModel): - # Nested model - convert recursively converted = _convert_entity_to_jsonld(item) if converted: - jsonld_values.append(converted) + linked_entities_jsonld.append(converted) + + # Try to extract DOI if present on the model (best-effort) + doi_val = None + if hasattr(item, "entityInfosciencePublication"): + ent = getattr(item, "entityInfosciencePublication") + if isinstance(ent, dict): + doi_val = ent.get("doi") + + elif isinstance(item, dict): + # Keep the raw dict converted normally + converted = _convert_entity_to_jsonld(item) + if converted: + linked_entities_jsonld.append(converted) + + # Look for infoscience entries and pull DOI + catalog = item.get("catalogType") + if catalog and isinstance(catalog, str) and catalog.lower() == "infoscience": + ent = item.get("entity") or item.get("entityInfosciencePublication") or {} + doi_val = None + if isinstance(ent, dict): + doi_val = ent.get("doi") + # Sometimes DOI is nested under identifiers + if not doi_val and "identifiers" in ent: + for ident in ent.get("identifiers", []): + if isinstance(ident, dict) and ident.get("type") == "doi": + doi_val = ident.get("value") + + if doi_val: + doi_str = str(doi_val).strip() + if doi_str and not doi_str.lower().startswith("http"): + citation_urls.append(f"https://doi.org/{doi_str}") + else: + citation_urls.append(doi_str) + else: - # Primitive or HttpUrl + # Fallback conversion converted = _convert_entity_to_jsonld(item) - if converted is not None: - jsonld_values.append(converted) + if converted: + linked_entities_jsonld.append(converted) + + if linked_entities_jsonld: + jsonld_entity[jsonld_key] = linked_entities_jsonld + if citation_urls: + # Merge with any existing citation entries + existing = jsonld_entity.get("schema:citation", []) + jsonld_entity["schema:citation"] = list(dict.fromkeys(existing + citation_urls)) + + continue + + # Special handling for relatedPublications - extract as schema:citation + if pydantic_key == "relatedPublications" and isinstance(value, list): + publication_urls = [] + + for item in value: + # Items should be strings (URLs) + if isinstance(item, str): + url = item.strip() + if url: + publication_urls.append(url) + elif isinstance(item, BaseModel): + # Handle if it's a Pydantic model (unlikely but defensive) + converted = _convert_entity_to_jsonld(item) + if isinstance(converted, str): + publication_urls.append(converted) + elif isinstance(item, dict): + # Handle if it's a dict with url field (unlikely but defensive) + url = item.get("url") or item.get("@id") + if url: + publication_urls.append(str(url).strip()) + + # Also preserve the original relatedPublications field + if publication_urls: + jsonld_entity[jsonld_key] = publication_urls + + # Add to schema:citation + existing = jsonld_entity.get("schema:citation", []) + jsonld_entity["schema:citation"] = list(dict.fromkeys(existing + publication_urls)) + + continue + + # Handle lists + if isinstance(value, list): + jsonld_values = [] + for item in value: + if isinstance(item, BaseModel): + item_model_name = item.__class__.__name__ + + # For Person objects in author field, just output IRI reference + if item_model_name == "Person" and pydantic_key == "author": + person_iri = _generate_person_iri(item) + if person_iri: + jsonld_values.append({"@id": person_iri}) + # For Affiliation models, extract organizationId (URL) or name + elif item_model_name == "Affiliation" and pydantic_key in ["affiliations", "affiliation"]: + org_id = getattr(item, "organizationId", None) + if org_id and isinstance(org_id, str) and (org_id.startswith("http://") or org_id.startswith("https://")): + jsonld_values.append({"@id": org_id}) + elif hasattr(item, "name") and item.name: + jsonld_values.append(item.name) + else: + # Nested model - convert recursively + converted = _convert_entity_to_jsonld(item) + if converted: + jsonld_values.append(converted) + else: + # Special handling for affiliation objects present as dicts + if pydantic_key in ["affiliations", "affiliation"] and isinstance(item, dict): + # Prefer organizationId if it's a URL, otherwise use name + org_id = item.get("organizationId") + affiliation_value = None + + if org_id and isinstance(org_id, str) and (org_id.startswith("http://") or org_id.startswith("https://")): + affiliation_value = {"@id": org_id} + elif item.get("name"): + affiliation_value = item.get("name") + + if affiliation_value: + jsonld_values.append(affiliation_value) + + # Author dicts may contain an explicit id we should use as IRI + elif pydantic_key == "author" and isinstance(item, dict) and item.get("id"): + jsonld_values.append({"@id": item.get("id")}) + + else: + # Primitive or HttpUrl or other - fallback to normal conversion + converted = _convert_entity_to_jsonld(item) + if converted is not None: + jsonld_values.append(converted) if jsonld_values: jsonld_entity[jsonld_key] = jsonld_values # Handle nested models elif isinstance(value, BaseModel): - converted = _convert_entity_to_jsonld(value) - if converted: - jsonld_entity[jsonld_key] = converted + nested_model_name = value.__class__.__name__ + + # For Person objects in author field, just output IRI reference + if nested_model_name == "Person" and pydantic_key == "author": + person_iri = _generate_person_iri(value) + if person_iri: + jsonld_entity[jsonld_key] = {"@id": person_iri} + else: + converted = _convert_entity_to_jsonld(value) + if converted: + jsonld_entity[jsonld_key] = converted + + # Handle dictionaries that might be serialized BaseModels + elif isinstance(value, dict): + # Special case: if this is 'entity' field in AcademicCatalogRelation, + # it might be a dict representation of InfosciencePublication/Author/Lab + # Try to detect and map the fields appropriately + if pydantic_key == "entity" and model_name == "AcademicCatalogRelation": + # Determine the entity type and apply appropriate mapping + entity_dict = {} + + # Detect which type based on fields present + entity_mapping = None + if "title" in value and "authors" in value: + # InfosciencePublication + entity_mapping = PYDANTIC_TO_ZOD_MAPPING.get( + "InfosciencePublication", + {}, + ) + entity_dict["@type"] = "schema:ScholarlyArticle" + elif "profile_url" in value or ( + "uuid" in value and "email" in value and "orcid" in value + ): + # InfoscienceAuthor + entity_mapping = PYDANTIC_TO_ZOD_MAPPING.get( + "InfoscienceAuthor", + {}, + ) + entity_dict["@type"] = "schema:Person" + elif "parent_organization" in value or ("research_areas" in value): + # InfoscienceLab + entity_mapping = PYDANTIC_TO_ZOD_MAPPING.get( + "InfoscienceLab", + {}, + ) + entity_dict["@type"] = "schema:Organization" + elif "name" in value: + # CatalogEntity + entity_mapping = PYDANTIC_TO_ZOD_MAPPING.get( + "CatalogEntity", + {}, + ) + entity_dict["@type"] = "pulse:CatalogEntity" + + if entity_mapping: + # Map the fields using the detected mapping + for entity_key, entity_value in value.items(): + if entity_value is not None: + mapped_key = entity_mapping.get(entity_key, entity_key) + # Recursively convert nested values + converted_value = _convert_entity_to_jsonld( + entity_value, + ) + entity_dict[mapped_key] = ( + converted_value + if converted_value is not None + else entity_value + ) + jsonld_entity[jsonld_key] = entity_dict + else: + # Fallback: use dict as-is + jsonld_entity[jsonld_key] = value + else: + # Regular dict - use as-is but try to convert nested values + jsonld_entity[jsonld_key] = value # Handle other types else: @@ -593,19 +1109,449 @@ def _convert_entity_to_jsonld( converted = _convert_entity_to_jsonld(value) if converted is not None: jsonld_entity[jsonld_key] = converted - + return jsonld_entity + # Collect all Person entities encountered during conversion + person_entities = {} # Dict to deduplicate by IRI + + def _collect_and_convert_person(person_obj: Any) -> Optional[str]: + """Convert a Person object and collect it, returning its IRI.""" + person_iri = _generate_person_iri(person_obj) + if not person_iri: + return None + + # If we haven't seen this person yet, convert and store them + if person_iri not in person_entities: + person_entity = _convert_entity_to_jsonld(person_obj, entity_id=person_iri) + if person_entity: + person_entities[person_iri] = person_entity + + return person_iri + # Convert the main object main_entity = _convert_entity_to_jsonld(pydantic_obj, base_url) if not main_entity: return {} + # Collect Person entities from the main object + # We need to traverse and collect all Person objects + def _collect_persons_from_obj(obj: Any): + """Recursively collect Person objects from the data structure.""" + if isinstance(obj, BaseModel): + if obj.__class__.__name__ == "Person": + _collect_and_convert_person(obj) + # Traverse all fields + for key, value in obj: + _collect_persons_from_obj(value) + elif isinstance(obj, list): + for item in obj: + _collect_persons_from_obj(item) + elif isinstance(obj, dict): + for value in obj.values(): + _collect_persons_from_obj(value) + + # Collect all Person entities + _collect_persons_from_obj(pydantic_obj) + + # Build the graph with main entity and all collected persons + graph_entities = [main_entity] + graph_entities.extend(person_entities.values()) + # Return as JSON-LD graph structure result = { "@context": context, - "@graph": [main_entity], + "@graph": graph_entities, } + # Ensure all values are JSON-serializable before returning + return _make_json_serializable(result) + + +############################################################ +# +# Simplified Model Generation for vLLM Compatibility +# +############################################################ + +# Module-level cache for generated simplified models +_SIMPLIFIED_MODEL_CACHE: Dict[ + Type[BaseModel], + Tuple[Type[BaseModel], Dict[str, Any]], +] = {} + + +def _is_pydantic_model(annotation: Any) -> bool: + """Check if an annotation is a Pydantic BaseModel class.""" + return ( + isinstance(annotation, type) + and issubclass(annotation, BaseModel) + and annotation is not BaseModel + ) + + +def _get_type_name(type_obj: Any) -> str: + """Get a clean type name for field naming.""" + if isinstance(type_obj, type): + # Capitalize primitive types for better field names + if type_obj is str: + return "String" + elif type_obj is int: + return "Int" + elif type_obj is float: + return "Float" + elif type_obj is bool: + return "Bool" + return type_obj.__name__ + type_str = str(type_obj).replace("typing.", "").replace("'", "") + # Capitalize common types + if type_str == "str": + return "String" + elif type_str == "int": + return "Int" + elif type_str == "float": + return "Float" + elif type_str == "bool": + return "Bool" + return type_str + + +def _simplify_type( + annotation: Any, + memo: Dict[Type[BaseModel], Tuple[Type[BaseModel], Dict[str, Any]]], + union_metadata: Dict[str, Any], + field_name: str, +) -> Tuple[Any, Optional[str]]: + """ + Convert a type annotation to a simplified type. + + Returns: + Tuple of (simplified_type, description_addition) + """ + origin = get_origin(annotation) + + # Handle Optional (Union with None) + if origin is Union: + args = get_args(annotation) + # Filter out NoneType + non_none_args = [arg for arg in args if arg is not type(None)] + + if len(non_none_args) == 0: + return (Optional[str], " (Original type: None)") + + # If only one non-None type, simplify it + if len(non_none_args) == 1: + simplified, desc = _simplify_type( + non_none_args[0], + memo, + union_metadata, + field_name, + ) + return (Optional[simplified], desc) + + # Multiple types in Union - need to split into separate fields + # Store metadata for reconciliation + union_info = { + "original_field": field_name, + "types": non_none_args, + "fields": {}, + } + + simplified_types = [] + for union_type in non_none_args: + simplified, desc = _simplify_type( + union_type, + memo, + union_metadata, + field_name, + ) + type_name = _get_type_name(union_type) + field_suffix = type_name.replace("typing.", "").replace("'", "") + new_field_name = f"{field_name}{field_suffix}" + union_info["fields"][new_field_name] = { + "type": union_type, + "simplified_type": simplified, + "description": desc, + } + simplified_types.append((new_field_name, simplified, desc)) + + # Store in union_metadata + if field_name not in union_metadata: + union_metadata[field_name] = [] + union_metadata[field_name].append(union_info) + + # Return None to indicate this field should be split + return (None, None) + + # Handle Dict types + if origin is dict or origin is DictType: + args = get_args(annotation) + if len(args) >= 2: + key_type = args[0] + value_type = args[1] + + # Simplify both key and value types + simplified_key, key_desc = _simplify_type( + key_type, + memo, + union_metadata, + field_name, + ) + simplified_value, value_desc = _simplify_type( + value_type, + memo, + union_metadata, + field_name, + ) + + # Return Dict with simplified types + return ( + DictType[simplified_key, simplified_value], + f" (Original type: Dict[{key_type}, {value_type}])", + ) + return (DictType[str, Any], " (Original type: Dict)") + + # Handle List types + if origin is list or origin is ListType: + args = get_args(annotation) + if args: + inner_type = args[0] + inner_origin = get_origin(inner_type) + + # Check if inner type is a Union that needs splitting + if inner_origin is Union: + inner_args = get_args(inner_type) + non_none_inner_args = [ + arg for arg in inner_args if arg is not type(None) + ] + + if len(non_none_inner_args) > 1: + # List[Union[A, B]] - split into separate List fields + union_info = { + "original_field": field_name, + "types": non_none_inner_args, + "fields": {}, + "is_list": True, + } + + for union_type in non_none_inner_args: + simplified, desc = _simplify_type( + union_type, + memo, + union_metadata, + field_name, + ) + type_name = _get_type_name(union_type) + field_suffix = type_name.replace("typing.", "").replace("'", "") + new_field_name = f"{field_name}{field_suffix}" + union_info["fields"][new_field_name] = { + "type": union_type, + "simplified_type": ListType[simplified], + "description": desc, + } + + # Store in union_metadata + if field_name not in union_metadata: + union_metadata[field_name] = [] + union_metadata[field_name].append(union_info) + + # Return None to indicate this field should be split + return (None, None) + + # Normal List handling + simplified_inner, desc = _simplify_type( + inner_type, + memo, + union_metadata, + field_name, + ) + if simplified_inner is None: + # This shouldn't happen after the Union check above, but handle it + return (None, None) + return (ListType[simplified_inner], desc) + return (ListType[str], " (Original type: List)") + + # Handle HttpUrl -> str + if annotation is HttpUrl or ( + isinstance(annotation, type) and issubclass(annotation, HttpUrl) + ): + return ( + str, + " (Original type: HttpUrl, format: string URL like 'https://example.com/path')", + ) + + # Handle date -> str + if annotation is date: + return (str, " (Original type: date, ISO format: YYYY-MM-DD)") + + # Handle datetime -> str + if annotation is datetime: + return (str, " (Original type: datetime, ISO format)") + + # Handle Enum -> Literal with enum values + if isinstance(annotation, type) and issubclass(annotation, Enum): + enum_values = [e.value for e in annotation] + # Convert to Literal type with the specific enum values + # This ensures the LLM must use one of these exact strings + # Create Literal dynamically with unpacked values + if enum_values: + # Use eval to create Literal with unpacked values + # This is safe since enum_values come from the Enum class + # Pass Literal in the namespace so eval can access it + literal_type = eval( + f"Literal[{', '.join(repr(v) for v in enum_values)}]", + {"Literal": Literal}, + ) + else: + literal_type = str + return ( + literal_type, + f" (Original type: {annotation.__name__} enum, values: {enum_values})", + ) + + # Handle Pydantic models - recursively simplify + if _is_pydantic_model(annotation): + simplified_model, _ = create_simplified_model(annotation, memo) + return (simplified_model, f" (Original type: {annotation.__name__})") + + # Primitive types - keep as-is + if annotation in (str, int, float, bool): + return (annotation, None) + + # Default: convert to str + return (str, f" (Original type: {annotation})") + + +def create_simplified_model( + source_model: Type[BaseModel], + memo: Optional[ + Dict[Type[BaseModel], Tuple[Type[BaseModel], Dict[str, Any]]] + ] = None, + field_filter: Optional[list[str]] = None, +) -> Tuple[Type[BaseModel], Dict[str, Any]]: + """ + Dynamically create a simplified Pydantic model from a source model. + + Converts complex Pydantic types (HttpUrl, date, datetime, Enum) to primitives (str) + and splits Union types into separate fields for vLLM compatibility. + + Args: + source_model: The source Pydantic model class to simplify (e.g., SoftwareSourceCode) + memo: Optional memoization cache (uses module-level cache if None) + field_filter: Optional list of field names to include. If None, includes all fields. + + Returns: + Tuple of (simplified_model_class, union_metadata) + - simplified_model_class: The dynamically created simplified model + - union_metadata: Dict mapping original field names to Union field info for reconciliation + + Example: + SimplifiedSoftwareSourceCode, union_meta = create_simplified_model(SoftwareSourceCode) + # Use SimplifiedSoftwareSourceCode as output_type in PydanticAI agent + + # With field filtering: + fields_to_extract = ["name", "description", "discipline", "repositoryType"] + SimplifiedModel, union_meta = create_simplified_model(SoftwareSourceCode, field_filter=fields_to_extract) + """ + # Use module-level cache if memo not provided + # Cache key includes field_filter to avoid collisions + use_module_cache = memo is None + if use_module_cache: + cache_key = (source_model, tuple(field_filter) if field_filter else None) + if cache_key in _SIMPLIFIED_MODEL_CACHE: + return _SIMPLIFIED_MODEL_CACHE[cache_key] + memo = {} + + # Check memoization cache + if source_model in memo: + return memo[source_model] + + # Track union metadata for this model + union_metadata: Dict[str, Any] = {} + + # Get all fields from source model + new_fields: Dict[str, Any] = {} + + for field_name, field_info in source_model.model_fields.items(): + # Filter fields if field_filter is provided + if field_filter is not None and field_name not in field_filter: + continue + annotation = field_info.annotation + default = field_info.default if field_info.default is not ... else None + default_factory = ( + field_info.default_factory + if field_info.default_factory is not ... + else None + ) + + # Simplify the type (this may populate union_metadata) + simplified_type, desc_addition = _simplify_type( + annotation, + memo, + union_metadata, + field_name, + ) + + # If simplified_type is None, it's a Union that needs splitting + # Skip this field - it will be split into separate fields later + if simplified_type is None: + continue + + # Double-check: if field_name is now in union_metadata, skip it + # (this handles the case where union_metadata was populated during _simplify_type) + if field_name in union_metadata: + continue + + # Build description + description = field_info.description or "" + if desc_addition: + description = f"{description}{desc_addition}".strip() + + # Create Field with description + if default_factory is not None: + # Handle default_factory (e.g., default_factory=list) + # For LLM compatibility, convert to Optional with default=None + # This allows LLMs to return None instead of empty lists + # We'll convert None back to empty lists when reconstructing the full model + new_fields[field_name] = ( + Optional[simplified_type], + Field(default=None, description=description), + ) + elif default is None and not field_info.is_required(): + new_fields[field_name] = ( + Optional[simplified_type], + Field(default=None, description=description), + ) + elif default is not None: + new_fields[field_name] = ( + simplified_type, + Field(default=default, description=description), + ) + else: + new_fields[field_name] = (simplified_type, Field(description=description)) + + # Handle Union field splitting - add separate fields + for field_name, union_info_list in union_metadata.items(): + for union_info in union_info_list: + for new_field_name, field_data in union_info["fields"].items(): + simplified_type = field_data["simplified_type"] + description = f"Part of Union field '{field_name}'. {field_data['description'] or ''}" + description = description.strip() + new_fields[new_field_name] = ( + Optional[simplified_type], + Field(default=None, description=description), + ) + + # Create the simplified model + simplified_model_name = f"Simplified{source_model.__name__}" + simplified_model = create_model(simplified_model_name, **new_fields) + + # Cache the result + result = (simplified_model, union_metadata) + memo[source_model] = result + if use_module_cache: + cache_key = (source_model, tuple(field_filter) if field_filter else None) + _SIMPLIFIED_MODEL_CACHE[cache_key] = result + return result diff --git a/src/data_models/infoscience.py b/src/data_models/infoscience.py index 7114b60..7345073 100644 --- a/src/data_models/infoscience.py +++ b/src/data_models/infoscience.py @@ -3,7 +3,6 @@ """ import re -from datetime import date from typing import List, Literal, Optional from pydantic import BaseModel, Field, HttpUrl, field_validator @@ -67,7 +66,9 @@ def validate_publication_url(cls, v): if isinstance(v, str): pattern = r"^https://infoscience\.epfl\.ch/entities/publication/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" if not re.match(pattern, v): - raise ValueError(f"Invalid Infoscience publication URL format: {v}. Expected: https://infoscience.epfl.ch/entities/publication/{{uuid}}") + raise ValueError( + f"Invalid Infoscience publication URL format: {v}. Expected: https://infoscience.epfl.ch/entities/publication/{{uuid}}", + ) return v def to_markdown(self) -> str: @@ -84,6 +85,10 @@ def to_markdown(self) -> str: if self.uuid: md_parts.append(f"*UUID:* {self.uuid}") + # URL (explicit field for LLM extraction) + if self.url: + md_parts.append(f"*URL:* {self.url}") + # Authors if self.authors: authors_str = ", ".join(self.authors) @@ -159,7 +164,7 @@ def validate_orcid(cls, v): """Validate ORCID format and convert ID to URL if needed.""" if v is None: return v - + if isinstance(v, str): # If it's already a URL, validate and return as-is (store as string) if v.startswith("http"): @@ -173,8 +178,10 @@ def validate_orcid(cls, v): if re.match(orcid_id_pattern, v): return v - raise ValueError(f"Invalid ORCID format: {v}. Expected format: 0000-0000-0000-0000 or https://orcid.org/0000-0000-0000-0000") - + raise ValueError( + f"Invalid ORCID format: {v}. Expected format: 0000-0000-0000-0000 or https://orcid.org/0000-0000-0000-0000", + ) + return v @field_validator("profile_url", mode="before") @@ -186,7 +193,9 @@ def validate_profile_url(cls, v): if isinstance(v, str): pattern = r"^https://infoscience\.epfl\.ch/entities/person/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" if not re.match(pattern, v): - raise ValueError(f"Invalid Infoscience person profile URL format: {v}. Expected: https://infoscience.epfl.ch/entities/person/{{uuid}}") + raise ValueError( + f"Invalid Infoscience person profile URL format: {v}. Expected: https://infoscience.epfl.ch/entities/person/{{uuid}}", + ) return v def to_markdown(self) -> str: @@ -203,6 +212,10 @@ def to_markdown(self) -> str: if self.uuid: md_parts.append(f"*UUID:* {self.uuid}") + # URL (explicit field for LLM extraction) + if self.profile_url: + md_parts.append(f"*URL:* {self.profile_url}") + # Affiliation if self.affiliation: md_parts.append(f"*Affiliation:* {self.affiliation}") @@ -218,11 +231,11 @@ def to_markdown(self) -> str: return "\n".join(md_parts) -class InfoscienceLab(BaseModel): - """Laboratory or organizational unit metadata from Infoscience""" +class InfoscienceOrgUnit(BaseModel): + """Organizational unit metadata from Infoscience""" - type: Literal["InfoscienceLab"] = Field( - default="InfoscienceLab", + type: Literal["InfoscienceOrgUnit"] = Field( + default="InfoscienceOrgUnit", description="Type discriminator for Infoscience entities", ) uuid: Optional[str] = Field( @@ -248,7 +261,9 @@ def validate_lab_url(cls, v): if isinstance(v, str): pattern = r"^https://infoscience\.epfl\.ch/entities/orgunit/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" if not re.match(pattern, v): - raise ValueError(f"Invalid Infoscience orgunit URL format: {v}. Expected: https://infoscience.epfl.ch/entities/orgunit/{{uuid}}") + raise ValueError( + f"Invalid Infoscience orgunit URL format: {v}. Expected: https://infoscience.epfl.ch/entities/orgunit/{{uuid}}", + ) return v parent_organization: Optional[str] = Field( @@ -278,6 +293,10 @@ def to_markdown(self) -> str: if self.uuid: md_parts.append(f"*UUID:* {self.uuid}") + # URL (explicit field for LLM extraction) + if self.url: + md_parts.append(f"*URL:* {self.url}") + # Parent organization if self.parent_organization: md_parts.append(f"*Part of:* {self.parent_organization}") @@ -315,7 +334,7 @@ class InfoscienceSearchResult(BaseModel): description="List of author results", default_factory=list, ) - labs: List[InfoscienceLab] = Field( + labs: List[InfoscienceOrgUnit] = Field( description="List of lab/organization results", default_factory=list, ) @@ -361,7 +380,9 @@ def to_markdown(self) -> str: else: md_parts.append("## ⚠️ STOP SEARCHING - No Results Found\n") - md_parts.append("**This search returned 0 results. The entity is NOT in Infoscience. Do NOT search again for this query because the results were 0.**") + md_parts.append( + "**This search returned 0 results. The entity is NOT in Infoscience. Do NOT search again for this query because the results were 0.**", + ) # Footer if md_parts and self.total_results > 0: diff --git a/src/data_models/academic_catalog.py b/src/data_models/linked_entities.py similarity index 55% rename from src/data_models/academic_catalog.py rename to src/data_models/linked_entities.py index a346483..bc3a960 100644 --- a/src/data_models/academic_catalog.py +++ b/src/data_models/linked_entities.py @@ -8,11 +8,11 @@ from __future__ import annotations from enum import Enum -from typing import Any, List, Optional, Union +from typing import Optional, Union from pydantic import BaseModel, Field -from .infoscience import InfoscienceAuthor, InfoscienceLab, InfosciencePublication +from .infoscience import InfoscienceAuthor, InfoscienceOrgUnit, InfosciencePublication class CatalogType(str, Enum): @@ -31,7 +31,7 @@ class EntityType(str, Enum): ORGUNIT = "orgunit" -class AcademicCatalogRelation(BaseModel): +class linkedEntitiesRelation(BaseModel): """ Relationship to an entity in an academic catalog. @@ -49,16 +49,51 @@ class AcademicCatalogRelation(BaseModel): description="Type of entity (publication, person, orgunit)", ) - entity: Union[ - InfosciencePublication, - InfoscienceAuthor, - InfoscienceLab, - dict[str, Any], + entity: Optional[ + Union[ + InfosciencePublication, + InfoscienceAuthor, + InfoscienceOrgUnit, + ] ] = Field( - description="Full entity details. For Infoscience: InfosciencePublication, " - "InfoscienceAuthor, or InfoscienceLab. For other catalogs: structured dict.", + default=None, + description="Full entity details. Can be InfosciencePublication, InfoscienceAuthor, " + "or InfoscienceOrgUnit depending on entityType. Can be None if only URL/UUID available.", ) + entityInfosciencePublication: Optional[InfosciencePublication] = Field( + default=None, + description="Full entity details for an Infoscience publication. Required if entityType is 'publication'.", + ) + + entityInfoscienceAuthor: Optional[InfoscienceAuthor] = Field( + default=None, + description="Full entity details for an Infoscience author. Required if entityType is 'person'.", + ) + + entityInfoscienceOrgUnit: Optional[InfoscienceOrgUnit] = Field( + default=None, + description="Full entity details for an Infoscience organizational unit. Required if entityType is 'orgunit'.", + ) + + def model_post_init(self, __context: Any) -> None: + if self.entity: + if isinstance(self.entity, InfosciencePublication): + self.entityInfosciencePublication = self.entity + elif isinstance(self.entity, InfoscienceAuthor): + self.entityInfoscienceAuthor = self.entity + elif isinstance(self.entity, InfoscienceOrgUnit): + self.entityInfoscienceOrgUnit = self.entity + return super().model_post_init(__context) + + def model_dump(self, *args, **kwargs): + kwargs["exclude"] = { + "entityInfosciencePublication", + "entityInfoscienceAuthor", + "entityInfoscienceOrgUnit", + } + return super().model_dump(*args, **kwargs) + confidence: float = Field( description="Confidence score (0.0-1.0) for this relationship", ge=0.0, @@ -70,58 +105,51 @@ class AcademicCatalogRelation(BaseModel): description="Explanation of why this entity is related and how it was found", ) - matchedOn: Optional[List[str]] = Field( - description="Fields used to match this entity (e.g., ['name', 'email'], ['doi'])", - default_factory=list, - ) - - def get_display_name(self) -> str: - """Get a display name for this entity.""" - if isinstance( - self.entity, - (InfosciencePublication, InfoscienceLab, InfoscienceAuthor), - ): - return getattr(self.entity, "title", None) or getattr( - self.entity, - "name", - "Unknown", - ) - if isinstance(self.entity, dict): - return self.entity.get("title") or self.entity.get("name", "Unknown") - return "Unknown" - - def get_url(self) -> Optional[str]: - """Get the URL for this entity if available.""" - if isinstance(self.entity, InfosciencePublication): - return self.entity.url - if isinstance(self.entity, InfoscienceAuthor): - return self.entity.profile_url - if isinstance(self.entity, InfoscienceLab): - return self.entity.url - if isinstance(self.entity, dict): - return self.entity.get("url") or self.entity.get("profile_url") - return None - def to_markdown(self) -> str: """Convert relation to markdown format for logging/display.""" + entity = None + if self.entityType == EntityType.PUBLICATION: + entity = self.entityInfosciencePublication + elif self.entityType == EntityType.PERSON: + entity = self.entityInfoscienceAuthor + elif self.entityType == EntityType.ORGUNIT: + entity = self.entityInfoscienceOrgUnit + + def get_display_name() -> str: + """Get a display name for this entity.""" + if entity is None: + return "Unknown" + if hasattr(entity, "title"): + return entity.title or "Unknown" + if hasattr(entity, "name"): + return entity.name or "Unknown" + return "Unknown" + + def get_url() -> Optional[str]: + """Get the URL for this entity if available.""" + if entity is None: + return None + if hasattr(entity, "url"): + return str(entity.url) if entity.url else None + if hasattr(entity, "profile_url"): + return str(entity.profile_url) if entity.profile_url else None + return None + lines = [] lines.append(f"**{self.catalogType.value}** - {self.entityType.value}") - lines.append(f"*Entity:* {self.get_display_name()}") + lines.append(f"*Entity:* {get_display_name()}") - url = self.get_url() + url = get_url() if url: lines.append(f"*URL:* {url}") lines.append(f"*Confidence:* {self.confidence:.2f}") lines.append(f"*Justification:* {self.justification}") - if self.matchedOn: - lines.append(f"*Matched on:* {', '.join(self.matchedOn)}") - return "\n".join(lines) -class AcademicCatalogEnrichmentResult(BaseModel): +class linkedEntitiesEnrichmentResult(BaseModel): """ Result from academic catalog enrichment agent. @@ -131,17 +159,17 @@ class AcademicCatalogEnrichmentResult(BaseModel): - organization_relations: Relations for each organization (orgunit profiles, publications) """ - repository_relations: list[AcademicCatalogRelation] = Field( + repository_relations: list[linkedEntitiesRelation] = Field( description="Relations found for the repository itself (publications about the repository name/project)", default_factory=list, ) - author_relations: dict[str, list[AcademicCatalogRelation]] = Field( + author_relations: dict[str, list[linkedEntitiesRelation]] = Field( description="Relations found for each author, keyed by author name as provided", default_factory=dict, ) - organization_relations: dict[str, list[AcademicCatalogRelation]] = Field( + organization_relations: dict[str, list[linkedEntitiesRelation]] = Field( description="Relations found for each organization, keyed by organization name as provided", default_factory=dict, ) @@ -174,7 +202,7 @@ class AcademicCatalogEnrichmentResult(BaseModel): # Backward compatibility - aggregates all relations @property - def relations(self) -> list[AcademicCatalogRelation]: + def relations(self) -> list[linkedEntitiesRelation]: """Get all relations combined (for backward compatibility).""" all_relations = list(self.repository_relations) for author_rels in self.author_relations.values(): @@ -186,26 +214,26 @@ def relations(self) -> list[AcademicCatalogRelation]: def get_by_catalog( self, catalog_type: CatalogType, - ) -> list[AcademicCatalogRelation]: + ) -> list[linkedEntitiesRelation]: """Get relations from a specific catalog.""" return [r for r in self.relations if r.catalogType == catalog_type] def get_by_entity_type( self, entity_type: EntityType, - ) -> list[AcademicCatalogRelation]: + ) -> list[linkedEntitiesRelation]: """Get relations of a specific entity type.""" return [r for r in self.relations if r.entityType == entity_type] - def get_publications(self) -> list[AcademicCatalogRelation]: + def get_publications(self) -> list[linkedEntitiesRelation]: """Get all publication relations.""" return self.get_by_entity_type(EntityType.PUBLICATION) - def get_persons(self) -> list[AcademicCatalogRelation]: + def get_persons(self) -> list[linkedEntitiesRelation]: """Get all person relations.""" return self.get_by_entity_type(EntityType.PERSON) - def get_orgunits(self) -> list[AcademicCatalogRelation]: + def get_orgunits(self) -> list[linkedEntitiesRelation]: """Get all organizational unit relations.""" return self.get_by_entity_type(EntityType.ORGUNIT) @@ -226,7 +254,22 @@ def to_markdown(self) -> str: if self.relations: lines.append("### Relations\n") for idx, relation in enumerate(self.relations, 1): - lines.append(f"#### {idx}. {relation.get_display_name()}") + entity = None + if relation.entityType == EntityType.PUBLICATION: + entity = relation.entityInfosciencePublication + elif relation.entityType == EntityType.PERSON: + entity = relation.entityInfoscienceAuthor + elif relation.entityType == EntityType.ORGUNIT: + entity = relation.entityInfoscienceOrgUnit + + display_name = "Unknown" + if entity: + if hasattr(entity, "title"): + display_name = entity.title or "Unknown" + elif hasattr(entity, "name"): + display_name = entity.name or "Unknown" + + lines.append(f"#### {idx}. {display_name}") lines.append(relation.to_markdown()) lines.append("") # Empty line between relations else: diff --git a/src/data_models/models.py b/src/data_models/models.py index 596f265..867b629 100644 --- a/src/data_models/models.py +++ b/src/data_models/models.py @@ -2,25 +2,43 @@ General data models """ +import hashlib from enum import Enum from typing import ( TYPE_CHECKING, - Any, List, Literal, Optional, - Union, ) -from pydantic import BaseModel, Field, HttpUrl, field_validator +from pydantic import BaseModel, Field, HttpUrl, field_validator, model_validator if TYPE_CHECKING: - from .academic_catalog import AcademicCatalogRelation + from .linked_entities import linkedEntitiesRelation + + +class Affiliation(BaseModel): + """Structured affiliation with provenance tracking""" + + name: str = Field( + description="Organization name (e.g., 'Swiss Data Science Center', 'EPFL')", + ) + organizationId: Optional[str] = Field( + default=None, + description="Organization identifier: ROR ID, GitHub handle, or internal ID", + ) + source: str = Field( + description="Data source: 'gimie', 'orcid', 'agent_org_enrichment', 'agent_user_enrichment', 'github_profile', 'email_domain'", + ) class Person(BaseModel): """Person model representing an individual author or contributor""" + id: str = Field( + default="", + description="Unique identifier for the person. Link to the person's URL or internal ID", + ) # Type discriminator type: Literal["Person"] = Field( default="Person", @@ -29,43 +47,41 @@ class Person(BaseModel): # Core identity fields name: str = Field(description="Person's name") - email: Optional[Union[str, List[str]]] = Field( + emails: Optional[List[str]] = Field( description="Email address(es) - can be a single string or a list of strings", + default_factory=list, + ) + githubId: Optional[str] = Field( + description="GitHub username/handle (e.g., 'octocat')", default=None, ) orcid: Optional[str] = Field( - description="ORCID identifier (format: 0000-0000-0000-0000 or https://orcid.org/0000-0000-0000-0000). Examples: '0000-0002-1234-5678', '0000-0000-0000-000X'", + description="ORCID identifier (format: 0000-0000-0000-0000).", default=None, ) - gitAuthorIds: Optional[List[str]] = Field( - description="List of git author identifiers mapping to this person", - default_factory=list, - ) + # gitAuthorIds: Optional[List[str]] = Field( + # description="List of git author identifiers mapping to this person", + # default_factory=list, + # ) # Affiliation fields - affiliations: List[str] = Field( - description="List of all identified affiliations (current and historical)", + affiliations: List[Affiliation] = Field( + description="List of current affiliations with provenance tracking", default_factory=list, ) - currentAffiliation: Optional[str] = Field( - description="Most recent or current affiliation", - default=None, - ) - affiliationHistory: List[dict[str, Any]] = Field( + affiliationHistory: List[str] = Field( description="Temporal affiliation information with start/end dates when available", default_factory=list, ) - # Additional metadata - contributionSummary: Optional[str] = Field( - description="Summary of the person's contributions to the repository", + # Provenance tracking + source: Optional[str] = Field( default=None, + description="Data source: 'gimie', 'llm', 'orcid', 'agent_user_enrichment', 'github_profile'", ) - biography: Optional[str] = Field( - description="Additional biographical or professional information", - default=None, - ) - academicCatalogRelations: Optional[List["AcademicCatalogRelation"]] = Field( + + # Additional metadata + linkedEntities: Optional[List["linkedEntitiesRelation"]] = Field( description="Relations to entities in academic catalogs (Infoscience, OpenAlex, EPFL Graph, etc.)", default_factory=list, ) @@ -75,10 +91,10 @@ class Person(BaseModel): def validate_orcid(cls, v): """Validate ORCID format and convert ID to URL if needed.""" import re - + if v is None: return v - + if isinstance(v, str): # If it's already a URL, validate and return as-is (store as string) if v.startswith("http"): @@ -92,14 +108,57 @@ def validate_orcid(cls, v): if re.match(orcid_id_pattern, v): return v - raise ValueError(f"Invalid ORCID format: {v}. Expected format: 0000-0000-0000-0000 or https://orcid.org/0000-0000-0000-0000") - + raise ValueError( + f"Invalid ORCID format: {v}. Expected format: 0000-0000-0000-0000 or https://orcid.org/0000-0000-0000-0000", + ) + return v + def anonymize_emails(self, hash_length: int = 12) -> None: + """ + Replace the local part of each email with a SHA-256 hash while keeping the domain. + + Args: + hash_length: Number of hexadecimal characters to keep from the hash. Defaults to 12. + """ + if not self.emails: + return + + anonymized_emails: list[str] = [] + for email in self.emails: + if not email or "@" not in email: + anonymized_emails.append(email) + continue + + local_part, domain = email.split("@", 1) + if not domain: + anonymized_emails.append(email) + continue + + hashed_local = hashlib.sha256(local_part.encode("utf-8")).hexdigest() + if hash_length > 0: + hashed_local = hashed_local[:hash_length] + + anonymized_emails.append(f"{hashed_local}@{domain}") + + self.emails = anonymized_emails + + @model_validator(mode="after") + def anonymize_emails_after_validation(self): + """ + Automatically anonymize emails after Person model validation to ensure privacy. + """ + self.anonymize_emails() + return self + class Organization(BaseModel): """Organization model representing an institution or company""" + id: str = Field( + default="", + description="Unique identifier for the organization. Link to the organization's URL or internal ID", + ) # Type discriminator type: Literal["Organization"] = Field( default="Organization", @@ -108,19 +167,18 @@ class Organization(BaseModel): legalName: Optional[str] = None hasRorId: Optional[HttpUrl] = None - alternateNames: Optional[ - List[str] - ] = None # Other names the organization is known by organizationType: Optional[ str ] = None # university, research institute, lab, department, company, etc. - parentOrganization: Optional[ - str - ] = None # Name of parent organization if applicable - country: Optional[str] = None # Country where the organization is located - website: Optional[HttpUrl] = None # Official website attributionConfidence: Optional[float] = None # Confidence score (0.0 to 1.0) - academicCatalogRelations: Optional[List["AcademicCatalogRelation"]] = Field( + + # Provenance tracking + source: Optional[str] = Field( + default=None, + description="Data source: 'gimie', 'llm', 'agent_org_enrichment', 'github_profile'", + ) + + linkedEntities: Optional[List["linkedEntitiesRelation"]] = Field( description="Relations to entities in academic catalogs (Infoscience, OpenAlex, EPFL Graph, etc.)", default_factory=list, ) @@ -141,22 +199,6 @@ def validate_ror(cls, v): return f"https://ror.org/{v}" return v - @field_validator("website", mode="before") - @classmethod - def validate_website(cls, v): - """Ensure website URL is valid, fix common issues.""" - if v is None or v == "": - return None - if isinstance(v, str): - v = v.strip() - # If it doesn't start with http:// or https://, add https:// - if not v.startswith(("http://", "https://")): - v = f"https://{v}" - # Basic validation - if it doesn't look like a URL, return None - if " " in v or "." not in v: - return None - return v - class Discipline(str, Enum): SOCIAL_SCIENCES = "Social sciences" @@ -212,6 +254,7 @@ class RepositoryType(str, Enum): EDUCATIONAL_RESOURCE = "educational resource" DOCUMENTATION = "documentation" DATA = "data" + WEBPAGE = "webpage" OTHER = "other" diff --git a/src/data_models/organization.py b/src/data_models/organization.py index 37aa880..db60c44 100644 --- a/src/data_models/organization.py +++ b/src/data_models/organization.py @@ -21,7 +21,7 @@ from .repository import GitAuthor if TYPE_CHECKING: - from .academic_catalog import AcademicCatalogRelation + from .linked_entities import linkedEntitiesRelation class OrganizationLLMAnalysisResult(BaseModel): @@ -61,7 +61,7 @@ class OrganizationLLMAnalysisResult(BaseModel): ge=0.0, le=1.0, ) - academicCatalogRelations: Optional[List["AcademicCatalogRelation"]] = Field( + linkedEntities: Optional[List["linkedEntitiesRelation"]] = Field( description="Relations to entities in academic catalogs (Infoscience, OpenAlex, EPFL Graph, etc.)", default_factory=list, ) @@ -167,6 +167,10 @@ class Config: class GitHubOrganization(BaseModel): + id: str = Field( + default="", + description="Unique identifier for the organization. Link to the organization's GitHub profile URL.", + ) name: Optional[str] = None organizationType: Optional[str] = None githubOrganizationMetadata: Optional[GitHubOrganizationMetadata] = None @@ -179,7 +183,7 @@ class GitHubOrganization(BaseModel): relatedToEPFL: Optional[bool] = None relatedToEPFLJustification: Optional[str] = None relatedToEPFLConfidence: Optional[float] = None # Confidence score (0.0 to 1.0) - academicCatalogRelations: Optional[List["AcademicCatalogRelation"]] = Field( + linkedEntities: Optional[List["linkedEntitiesRelation"]] = Field( description="Relations to entities in academic catalogs (Infoscience, OpenAlex, EPFL Graph, etc.)", default_factory=list, ) diff --git a/src/data_models/repository.py b/src/data_models/repository.py index 3ba0cb1..39edfb0 100644 --- a/src/data_models/repository.py +++ b/src/data_models/repository.py @@ -4,6 +4,7 @@ from __future__ import annotations +import hashlib import logging from datetime import date from enum import Enum @@ -37,7 +38,7 @@ ) if TYPE_CHECKING: - from .academic_catalog import AcademicCatalogRelation + from .linked_entities import linkedEntitiesRelation ##################################################################### # Debugging Utilities @@ -195,6 +196,10 @@ class Commits(BaseModel): class GitAuthor(BaseModel): + id: Optional[str] = Field( + default="", + description="SHA-256 hash of email and name combination", + ) name: str email: Optional[str] = None commits: Optional[Commits] = None @@ -219,10 +224,39 @@ def validate_commits_with_logging(cls, v): logger.warning(f"commits has unexpected type: {type(v)}") return v + @model_validator(mode="after") + def compute_id(self): + """Compute id as SHA-256 hash of email and name combination.""" + email = self.email or "" + name = self.name or "" + emailname = f"{email}{name}".encode() + self.id = hashlib.sha256(emailname).hexdigest() + return self + + def anonymize_email_local_part(self, hash_length: int = 12) -> None: + """ + Replace the local part of the email with a SHA-256 hash while keeping the domain. + + Args: + hash_length: Number of hexadecimal characters to keep from the hash. Defaults to 12. + """ + if not self.email or "@" not in self.email: + return + + local_part, domain = self.email.split("@", 1) + if not domain: + return + + hashed_local = hashlib.sha256(local_part.encode("utf-8")).hexdigest() + if hash_length > 0: + hashed_local = hashed_local[:hash_length] + + self.email = f"{hashed_local}@{domain}" + class InfoscienceEntity(BaseModel): """ - DEPRECATED: Use AcademicCatalogRelation instead. + DEPRECATED: Use linkedEntitiesRelation instead. Kept temporarily for backward compatibility during migration. """ @@ -234,53 +268,197 @@ class InfoscienceEntity(BaseModel): class SoftwareSourceCode(BaseModel): - name: Optional[str] = None - applicationCategory: Optional[list[str]] = None - citation: Optional[list[HttpUrl]] = [] - codeRepository: Optional[list[HttpUrl]] = [] - conditionsOfAccess: Optional[str] = None - dateCreated: Optional[date] = None - datePublished: Optional[date] = None - description: Optional[str] = None - featureList: Optional[list[str]] = None - image: Optional[list[Image]] = None - isAccessibleForFree: Optional[bool] = None - isBasedOn: Optional[HttpUrl] = None - isPluginModuleOf: Optional[list[str]] = None - license: Optional[Annotated[str, StringConstraints(pattern=r"spdx\.org.*")]] = None - author: Optional[list[Union[Person, Organization]]] = None - operatingSystem: Optional[list[str]] = None - programmingLanguage: Optional[list[str]] = None - softwareRequirements: Optional[list[str]] = None - processorRequirements: Optional[list[str]] = None - memoryRequirements: Optional[int] = None - requiresGPU: Optional[bool] = None - supportingData: Optional[list[DataFeed]] = [] - url: Optional[HttpUrl] = None - identifier: Optional[str] = None - hasAcknowledgements: Optional[str] = None - hasDocumentation: Optional[HttpUrl] = None - hasExecutableInstructions: Optional[str] = None - hasExecutableNotebook: Optional[List[ExecutableNotebook]] = [] - readme: Optional[HttpUrl] = None - hasFunding: Optional[List[FundingInformation]] = None - hasSoftwareImage: Optional[List[SoftwareImage]] = [] - imagingModality: Optional[List[str]] = None - discipline: Optional[List[Discipline]] = None - disciplineJustification: Optional[List[str]] = None - relatedDatasets: Optional[List[str]] = None - relatedPublications: Optional[List[str]] = None - relatedModels: Optional[List[str]] = None - relatedAPIs: Optional[List[str]] = None - relatedToOrganizations: Optional[List[Union[str, Organization]]] = None - relatedToOrganizationJustification: Optional[List[str]] = None - repositoryType: RepositoryType - repositoryTypeJustification: list[str] - relatedToEPFL: Optional[bool] = None - relatedToEPFLConfidence: Optional[float] = None # Confidence score (0.0 to 1.0) - relatedToEPFLJustification: Optional[str] = None - gitAuthors: Optional[List[GitAuthor]] = None - academicCatalogRelations: Optional[List["AcademicCatalogRelation"]] = Field( + id: str = Field( + default="", + description="Unique identifier for the repository. Link to the repository URL.", + ) + name: Optional[str] = Field( + default=None, + description="Repository name", + ) + applicationCategory: Optional[list[str]] = Field( + default=None, + description="Application categories", + ) + citation: Optional[list[HttpUrl]] = Field( + default=[], + description="Citations or references to related publications", + ) + codeRepository: Optional[list[HttpUrl]] = Field( + default=[], + description="Repository URLs", + ) + keywords: Optional[list[str]] = Field( + default=[], + description="Keywords or tags related to the software", + ) + # conditionsOfAccess: Optional[str] = Field( + # default=None, + # description="Conditions of access to the repository", + # ) + dateCreated: Optional[date] = Field( + default=None, + description="Creation date in ISO format (YYYY-MM-DD)", + ) + datePublished: Optional[date] = Field( + default=None, + description="Publication date in ISO format (YYYY-MM-DD)", + ) + description: Optional[str] = Field( + default=None, + description="Repository description or summary", + ) + featureList: Optional[list[str]] = Field( + default=None, + description="List of features or capabilities", + ) + # image: Optional[list[Image]] = Field( + # default=None, + # description="Images or screenshots of the software", + # ) + # isAccessibleForFree: Optional[bool] = Field( + # default=None, + # description="Whether the software is accessible for free", + # ) + # isBasedOn: Optional[HttpUrl] = Field( + # default=None, + # description="URL of the software this is based on", + # ) + # isPluginModuleOf: Optional[list[str]] = Field( + # default=None, + # description="List of software this is a plugin or module of", + # ) + license: Optional[str] = Field( + default=None, + description="License identifier (e.g., Apache-2.0, MIT)", + ) + author: Optional[list[Union[Person, Organization]]] = Field( + default=None, + description="List of authors/contributors", + ) + # operatingSystem: Optional[list[str]] = Field( + # default=None, + # description="Supported operating systems", + # ) + programmingLanguage: Optional[list[str]] = Field( + default=None, + description="Programming languages used in the repository", + ) + # softwareRequirements: Optional[list[str]] = Field( + # default=None, + # description="Software dependencies or requirements", + # ) + # processorRequirements: Optional[list[str]] = Field( + # default=None, + # description="Processor or CPU requirements", + # ) + # memoryRequirements: Optional[int] = Field( + # default=None, + # description="Memory requirements in bytes or MB", + # ) + # requiresGPU: Optional[bool] = Field( + # default=None, + # description="Whether the software requires a GPU", + # ) + # supportingData: Optional[list[DataFeed]] = Field( + # default=[], + # description="Supporting data feeds or datasets", + # ) + url: Optional[HttpUrl] = Field( + default=None, + description="Primary URL of the repository or project", + ) + # identifier: Optional[str] = Field( + # default=None, + # description="Unique identifier for the repository", + # ) + # hasAcknowledgements: Optional[str] = Field( + # default=None, + # description="Acknowledgements or credits", + # ) + # hasDocumentation: Optional[HttpUrl] = Field( + # default=None, + # description="URL to documentation", + # ) + # hasExecutableInstructions: Optional[str] = Field( + # default=None, + # description="Executable instructions or installation guide", + # ) + hasExecutableNotebook: Optional[list[ExecutableNotebook]] = Field( + default=[], + description="Executable notebooks (Jupyter, etc.) in the repository", + ) + readme: Optional[HttpUrl] = Field( + default=None, + description="URL to the README file", + ) + # hasFunding: Optional[list[FundingInformation]] = Field( + # default=None, + # description="Funding information and sources", + # ) + # hasSoftwareImage: Optional[list[SoftwareImage]] = Field( + # default=[], + # description="Software container images or Docker images", + # ) + # imagingModality: Optional[list[str]] = Field( + # default=None, + # description="Imaging modalities supported (for imaging software)", + # ) + discipline: Optional[list[Discipline]] = Field( + default=None, + description="Scientific disciplines", + ) + disciplineJustification: Optional[list[str]] = Field( + default=None, + description="Justification for each discipline", + ) + relatedDatasets: Optional[list[str]] = Field( + default=None, + description="Related datasets or data sources", + ) + relatedPublications: Optional[list[str]] = Field( + default=None, + description="Related publications or papers", + ) + relatedModels: Optional[list[str]] = Field( + default=None, + description="Related models or algorithms", + ) + relatedAPIs: Optional[list[str]] = Field( + default=None, + description="Related APIs or services", + ) + relatedToOrganizations: Optional[list[Union[str, Organization]]] = Field( + default=None, + description="Organizations related to the repository (hosting, funding, affiliation)", + ) + relatedToOrganizationJustification: Optional[list[str]] = Field( + default=None, + description="Justification for each organization relationship", + ) + repositoryType: RepositoryType = Field( + description="Repository type", + ) + repositoryTypeJustification: list[str] = Field( + description="Justification for repository type", + ) + relatedToEPFL: Optional[bool] = Field( + default=None, + description="Whether the repository is related to EPFL", + ) + relatedToEPFLConfidence: Optional[float] = Field( + default=None, + description="Confidence score (0.0 to 1.0) for EPFL relationship", + ) + relatedToEPFLJustification: Optional[str] = Field( + default=None, + description="Justification for EPFL relationship assessment", + ) + gitAuthors: Optional[list[GitAuthor]] = Field( + default=None, + description="Git commit authors", + ) + linkedEntities: Optional[list[linkedEntitiesRelation]] = Field( description="Relations to entities in academic catalogs (Infoscience, OpenAlex, EPFL Graph, etc.)", default_factory=list, ) @@ -302,16 +480,43 @@ def validate_author_with_logging(cls, v): valid_authors = [] for i, author in enumerate(v): + # Handle both dicts and Pydantic model instances + author_dict = None if isinstance(author, dict): + author_dict = author + elif hasattr(author, "model_dump"): + # Pydantic model instance - convert to dict + author_dict = author.model_dump() + elif hasattr(author, "name") or hasattr(author, "legalName"): + # Pydantic model instance without model_dump - try to access attributes + if hasattr(author, "name"): + author_dict = {"name": author.name} + # Copy other Person fields if available + for field in [ + "orcid", + "emails", + "affiliations", + "currentAffiliation", + ]: + if hasattr(author, field): + author_dict[field] = getattr(author, field) + elif hasattr(author, "legalName"): + author_dict = {"legalName": author.legalName} + # Copy other Organization fields if available + for field in ["hasRorId", "country", "website"]: + if hasattr(author, field): + author_dict[field] = getattr(author, field) + + if author_dict: # Check if it's a Person/EnrichedAuthor (has "name") or Organization (has "legalName") - name = author.get("name") - legal_name = author.get("legalName") + name = author_dict.get("name") + legal_name = author_dict.get("legalName") if name: # Person or EnrichedAuthor object # Check if it has enrichment fields to distinguish has_enrichment = any( - k in author + k in author_dict for k in [ "currentAffiliation", "affiliationHistory", @@ -329,14 +534,14 @@ def validate_author_with_logging(cls, v): # Neither Person nor Organization - check if it's an empty/invalid entry # Check if the entire entry is empty (all None values) has_any_value = any( - value is not None for value in author.values() + value is not None for value in author_dict.values() ) if has_any_value: # Has some data but missing name/legalName - this is a problem missing_names.append(f"Author {i+1}") logger.warning( - f" ⚠️ Author {i+1} missing name/legalName: {author}", + f" ⚠️ Author {i+1} missing name/legalName: {author_dict}", ) else: # Completely empty entry - will be filtered out later, no need to warn @@ -344,7 +549,12 @@ def validate_author_with_logging(cls, v): f" 🔕 Author {i+1} is completely empty (will be filtered)", ) else: - logger.warning(f" ⚠️ Author {i+1} is not a dict: {type(author)}") + # Not a dict and not a recognizable Pydantic model - keep as-is + # Pydantic will handle validation + logger.debug( + f" 📦 Author {i+1} is a Pydantic model instance: {type(author)}", + ) + valid_authors.append(str(type(author).__name__)) # Summary if missing_names: @@ -355,10 +565,24 @@ def validate_author_with_logging(cls, v): logger.debug(f" ✅ All {len(valid_authors)} authors have names") # Filter out completely empty entries (all fields are None) + # Also convert Pydantic model instances to dicts for consistency if v: cleaned_authors = [] for author in v: - if isinstance(author, dict): + # Convert Pydantic model instances to dicts + if hasattr(author, "model_dump"): + author_dict = author.model_dump() + # Check if the entry has any non-None values + has_any_value = any( + value is not None for value in author_dict.values() + ) + if has_any_value: + cleaned_authors.append(author_dict) + else: + logger.debug( + " 🗑️ Removing empty author entry (all None values)", + ) + elif isinstance(author, dict): # Check if the entry has any non-None values has_any_value = any( value is not None for value in author.values() @@ -370,7 +594,8 @@ def validate_author_with_logging(cls, v): " 🗑️ Removing empty author entry (all None values)", ) else: - # Keep non-dict entries (they'll be handled by Pydantic) + # Pydantic model instance without model_dump - keep as-is + # Pydantic will handle it cleaned_authors.append(author) if len(cleaned_authors) != len(v): @@ -414,12 +639,13 @@ def validate_git_authors_with_logging(cls, v): if isinstance(author, dict): name = author.get("name", "Unknown") email = author.get("email", "No email") + author_id = author.get("id", "No ID") commits = author.get("commits", {}) total_commits = ( commits.get("total", 0) if isinstance(commits, dict) else 0 ) logger.debug( - f" [{i+1}] {name} ({email}) - {total_commits} commits", + f" [{i+1}] {name} ({email}) [id: {author_id}] - {total_commits} commits", ) if len(v) > 5: @@ -525,6 +751,297 @@ def convert_pydantic_to_jsonld(self) -> dict: return convert_pydantic_to_jsonld(self, base_url=base_url) + def to_simplified_schema(self) -> dict: + """ + Convert selected SoftwareSourceCode fields to a simplified JSON schema + suitable for LLM agents that don't support complex types like HttpUrl or date. + + Only includes the following fields: + - name + - applicationCategory + - codeRepository (converted to strings) + - dateCreated (converted to string) + - license + - author (simplified to basic info) + - gitAuthors (simplified) + - discipline (converted to strings) + - repositoryType (converted to string) + - disciplineJustification + - repositoryTypeJustification + + Descriptions are automatically extracted from Field() definitions in the model. + + Returns: + Dictionary with simplified field definitions and expected types + """ + # Get field information from the model + model_fields = self.model_fields + + def get_field_description(field_name: str, default: str = "") -> str: + """Extract description from Field() definition, with fallback to default.""" + if field_name in model_fields: + field_info = model_fields[field_name] + if field_info.description: + return field_info.description + return default + + def get_field_required(field_name: str) -> bool: + """Check if field is required.""" + if field_name in model_fields: + field_info = model_fields[field_name] + return field_info.is_required() + return False + + schema = { + "name": { + "type": "string", + "description": get_field_description("name", "Repository name"), + "required": get_field_required("name"), + }, + "applicationCategory": { + "type": "array", + "items": {"type": "string"}, + "description": get_field_description( + "applicationCategory", + "Application categories", + ), + "required": get_field_required("applicationCategory"), + }, + "codeRepository": { + "type": "array", + "items": {"type": "string"}, + "description": get_field_description( + "codeRepository", + "Repository URLs as strings", + ), + "required": get_field_required("codeRepository"), + }, + "dateCreated": { + "type": "string", + "description": get_field_description( + "dateCreated", + "Creation date in ISO format (YYYY-MM-DD)", + ), + "required": get_field_required("dateCreated"), + }, + "license": { + "type": "string", + "description": get_field_description( + "license", + "License identifier (e.g., Apache-2.0, MIT)", + ), + "required": get_field_required("license"), + }, + "author": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "email": {"type": "string"}, + "orcid": {"type": "string"}, + "affiliations": { + "type": "array", + "items": {"type": "string"}, + }, + }, + }, + "description": get_field_description( + "author", + "List of authors/contributors", + ), + "required": get_field_required("author"), + }, + "gitAuthors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "email": {"type": "string"}, + "commits": { + "type": "object", + "properties": { + "count": {"type": "integer"}, + "firstCommit": {"type": "string"}, + "lastCommit": {"type": "string"}, + }, + }, + }, + }, + "description": get_field_description( + "gitAuthors", + "Git commit authors", + ), + "required": get_field_required("gitAuthors"), + }, + "discipline": { + "type": "array", + "items": {"type": "string"}, + "description": get_field_description( + "discipline", + "Scientific disciplines", + ), + "required": get_field_required("discipline"), + }, + "disciplineJustification": { + "type": "array", + "items": {"type": "string"}, + "description": get_field_description( + "disciplineJustification", + "Justification for each discipline", + ), + "required": get_field_required("disciplineJustification"), + }, + "repositoryType": { + "type": "string", + "description": get_field_description( + "repositoryType", + "Repository type", + ), + "required": get_field_required("repositoryType"), + }, + "repositoryTypeJustification": { + "type": "array", + "items": {"type": "string"}, + "description": get_field_description( + "repositoryTypeJustification", + "Justification for repository type", + ), + "required": get_field_required("repositoryTypeJustification"), + }, + } + return schema + + def to_simplified_dict(self) -> dict: + """ + Convert this SoftwareSourceCode instance to a simplified dictionary + with only primitive types (strings, numbers, lists, dicts). + + This is used to provide example data to LLM agents that need to understand + the expected output format but cannot handle complex Pydantic types. + + Returns: + Dictionary with simplified field values + """ + result = {} + + # name + if self.name is not None: + result["name"] = self.name + + # applicationCategory + if self.applicationCategory is not None: + result["applicationCategory"] = list(self.applicationCategory) + + # codeRepository - convert HttpUrl to strings + if self.codeRepository is not None: + result["codeRepository"] = [str(url) for url in self.codeRepository] + + # dateCreated - convert date to string + if self.dateCreated is not None: + result["dateCreated"] = self.dateCreated.isoformat() + + # license + if self.license is not None: + result["license"] = self.license + + # author - simplify to basic info + if self.author is not None: + simplified_authors = [] + for auth in self.author: + if isinstance(auth, Person): + author_dict = { + "name": auth.name, + } + if auth.emails: + author_dict["email"] = ( + auth.emails[0] + if isinstance(auth.emails, list) + else auth.emails + ) + if auth.orcid: + author_dict["orcid"] = auth.orcid + if auth.affiliations: + # Convert Affiliation objects to simple strings for simplified schema + author_dict["affiliations"] = [ + aff.name if hasattr(aff, "name") else str(aff) + for aff in auth.affiliations + ] + simplified_authors.append(author_dict) + elif isinstance(auth, dict): + # Already a dict, extract basic fields + author_dict = {} + if "name" in auth: + author_dict["name"] = auth["name"] + if "email" in auth: + author_dict["email"] = auth["email"] + if "orcid" in auth: + author_dict["orcid"] = auth["orcid"] + if "affiliations" in auth: + author_dict["affiliations"] = auth["affiliations"] + if author_dict: + simplified_authors.append(author_dict) + if simplified_authors: + result["author"] = simplified_authors + + # gitAuthors - simplify + if self.gitAuthors is not None: + simplified_git_authors = [] + for git_author in self.gitAuthors: + git_dict = { + "name": git_author.name, + } + if git_author.email: + git_dict["email"] = git_author.email + if git_author.commits: + git_dict["commits"] = { + "count": git_author.commits.count + if git_author.commits.count + else 0, + } + if git_author.commits.firstCommit: + git_dict["commits"]["firstCommit"] = ( + git_author.commits.firstCommit.isoformat() + if hasattr(git_author.commits.firstCommit, "isoformat") + else str(git_author.commits.firstCommit) + ) + if git_author.commits.lastCommit: + git_dict["commits"]["lastCommit"] = ( + git_author.commits.lastCommit.isoformat() + if hasattr(git_author.commits.lastCommit, "isoformat") + else str(git_author.commits.lastCommit) + ) + simplified_git_authors.append(git_dict) + if simplified_git_authors: + result["gitAuthors"] = simplified_git_authors + + # discipline - convert enum to strings + if self.discipline is not None: + result["discipline"] = [ + str(d.value) if hasattr(d, "value") else str(d) for d in self.discipline + ] + + # disciplineJustification + if self.disciplineJustification is not None: + result["disciplineJustification"] = list(self.disciplineJustification) + + # repositoryType - convert enum to string + if self.repositoryType is not None: + result["repositoryType"] = ( + self.repositoryType.value + if hasattr(self.repositoryType, "value") + else str(self.repositoryType) + ) + + # repositoryTypeJustification + if self.repositoryTypeJustification is not None: + result["repositoryTypeJustification"] = list( + self.repositoryTypeJustification, + ) + + return result + ##################################################################### # Usage Examples diff --git a/src/data_models/user.py b/src/data_models/user.py index f69277c..220115d 100644 --- a/src/data_models/user.py +++ b/src/data_models/user.py @@ -19,6 +19,7 @@ ) from .models import ( + Affiliation, Discipline, Organization, Person, @@ -26,7 +27,7 @@ from .repository import GitAuthor if TYPE_CHECKING: - from .academic_catalog import AcademicCatalogRelation + from .linked_entities import linkedEntitiesRelation class EnrichedAuthor(BaseModel): @@ -38,8 +39,8 @@ class EnrichedAuthor(BaseModel): description="Author's ORCID identifier (format: 0000-0000-0000-0000 or URL)", default=None, ) - affiliations: list[str] = Field( - description="List of all identified affiliations (current and historical)", + affiliations: list[Affiliation] = Field( + description="List of all identified affiliations with provenance", default_factory=list, ) currentAffiliation: Optional[str] = Field( @@ -62,7 +63,7 @@ class EnrichedAuthor(BaseModel): description="Additional biographical or professional information found", default=None, ) - academicCatalogRelations: list[AcademicCatalogRelation] = Field( + linkedEntities: list[linkedEntitiesRelation] = Field( description="Relations to entities in academic catalogs", default_factory=list, ) @@ -97,7 +98,7 @@ def convert_enriched_to_person(enriched: EnrichedAuthor) -> Person: # Additional metadata contributionSummary=enriched.contributionSummary, biography=enriched.additionalInfo, # Map additionalInfo to biography - academicCatalogRelations=enriched.academicCatalogRelations, + linkedEntities=enriched.linkedEntities, ) @@ -240,6 +241,10 @@ class GitHubUserMetadata(BaseModel): None, description="Profile README content if exists", ) + repositories: list[str] = Field( + default_factory=list, + description="List of public repositories", + ) @validator("orcid") def validate_orcid(cls, v): @@ -263,8 +268,18 @@ def validate_orcid(cls, v): @validator("email") def validate_email(cls, v): """Basic email validation""" - if v is not None and "@" not in v: - raise ValueError("Invalid email format") + if v is not None: + # Allow standard emails + if "@" in v: + return v + # Allow obfuscated emails (e.g. "user at domain dot com") + if " at " in v: + return v + # If it's not None but doesn't look like an email, we could either + # raise an error or just accept it. Given the goal is to extract metadata, + # accepting it is safer than crashing. + # raise ValueError("Invalid email format") + return v return v class Config: @@ -280,6 +295,10 @@ class Config: class GitHubUser(BaseModel): + id: str = Field( + default="", + description="Unique identifier for the user. Link to the user's GitHub profile URL.", + ) name: Optional[str] = None fullname: Optional[str] = None githubHandle: Optional[str] = None @@ -293,7 +312,7 @@ class GitHubUser(BaseModel): relatedToEPFL: Optional[bool] = None relatedToEPFLJustification: Optional[str] = None relatedToEPFLConfidence: Optional[float] = None # Confidence score (0.0 to 1.0) - academicCatalogRelations: Optional[list[AcademicCatalogRelation]] = Field( + linkedEntities: Optional[list[linkedEntitiesRelation]] = Field( description="Relations to entities in academic catalogs (Infoscience, OpenAlex, EPFL Graph, etc.)", default_factory=list, ) diff --git a/src/data_models/validation.py b/src/data_models/validation.py index fa88206..995043a 100644 --- a/src/data_models/validation.py +++ b/src/data_models/validation.py @@ -35,4 +35,3 @@ class ValidationResult(BaseModel): description="Any errors encountered during validation", default_factory=list, ) - diff --git a/src/llm/model_config.py b/src/llm/model_config.py index 83d5d8d..026b254 100644 --- a/src/llm/model_config.py +++ b/src/llm/model_config.py @@ -15,170 +15,452 @@ # Default model configurations MODEL_CONFIGS = { "run_llm_analysis": [ - # { - # "provider": "openai-compatible", - # "model": "openai/gpt-oss-120b", - # "base_url": "https://inference.rcp.epfl.ch/v1", - # "api_key_env": "RCP_TOKEN", - # "max_retries": 3, - # "temperature": 0.2, - # "max_tokens": 16000, - # "timeout": 600.0, - # }, { - "provider": "openai", - "model": "o4-mini", + "provider": "openai-compatible", + "model": "openai/gpt-oss-120b", + "base_url": "https://inference.rcp.epfl.ch/v1", + "api_key_env": "RCP_TOKEN", "max_retries": 3, "temperature": 0.2, "max_tokens": 16000, "timeout": 600.0, + "allow_tools": True, # Enable tool usage for this model }, + # { + # "provider": "openai", + # "model": "o4-mini", + # "max_retries": 3, + # "temperature": 0.2, + # "max_tokens": 16000, + # "timeout": 600.0, + # }, + # { + # "provider": "openrouter", + # "model": "google/gemini-2.5-flash", + # "max_retries": 3, + # "temperature": 0.2, + # "max_tokens": 16000, + # "timeout": 300.0, + # }, + # { + # "provider": "ollama", + # "model": "llama3.2", + # "base_url": "http://localhost:11434/v1", + # "max_retries": 2, + # "temperature": 0.3, + # "timeout": 600.0, + # }, + ], + "run_user_enrichment": [ { - "provider": "openrouter", - "model": "google/gemini-2.5-flash", - "max_retries": 3, - "temperature": 0.2, - "max_tokens": 16000, - "timeout": 300.0, - }, - { - "provider": "ollama", - "model": "llama3.2", - "base_url": "http://localhost:11434/v1", + "provider": "openai-compatible", + "model": "openai/gpt-oss-120b", + "base_url": "https://inference.rcp.epfl.ch/v1", + "api_key_env": "RCP_TOKEN", "max_retries": 2, - "temperature": 0.3, - "timeout": 600.0, + "temperature": 0.1, + "max_tokens": 8000, + "timeout": 300.0, }, - ], - "run_user_enrichment": [ # { - # "provider": "openai-compatible", - # "model": "openai/gpt-oss-120b", - # "base_url": "https://inference.rcp.epfl.ch/v1", - # "api_key_env": "RCP_TOKEN", + # "provider": "openai", + # "model": "o4-mini", # "max_retries": 2, # "temperature": 0.1, # "max_tokens": 8000, # "timeout": 300.0, # }, + # { + # "provider": "openrouter", + # "model": "google/gemini-2.5-flash", + # "max_retries": 3, + # "temperature": 0.2, + # "max_tokens": 16000, + # "timeout": 300.0, + # }, + ], + "run_organization_enrichment": [ { - "provider": "openai", - "model": "o4-mini", + "provider": "openai-compatible", + "model": "openai/gpt-oss-120b", + "base_url": "https://inference.rcp.epfl.ch/v1", + "api_key_env": "RCP_TOKEN", "max_retries": 2, "temperature": 0.1, "max_tokens": 8000, "timeout": 300.0, }, + # { + # "provider": "openai", + # "model": "o4-mini", + # "max_retries": 2, + # "temperature": 0.1, + # "max_tokens": 8000, + # "timeout": 300.0, + # }, + # { + # "provider": "openrouter", + # "model": "google/gemini-2.5-flash", + # "max_retries": 3, + # "temperature": 0.2, + # "max_tokens": 16000, + # "timeout": 300.0, + # }, + ], + "run_linked_entities_enrichment": [ { - "provider": "openrouter", - "model": "google/gemini-2.5-flash", + "provider": "openai-compatible", + "model": "openai/gpt-oss-120b", + "base_url": "https://inference.rcp.epfl.ch/v1", + "api_key_env": "RCP_TOKEN", "max_retries": 3, - "temperature": 0.2, - "max_tokens": 16000, + "temperature": 0.1, + "max_tokens": 12000, "timeout": 300.0, + "allow_tools": True, # Uses Infoscience search tools }, + # { + # "provider": "openai", + # "model": "o4-mini", + # "max_retries": 3, + # "temperature": 0.1, + # "max_tokens": 12000, + # "timeout": 300.0, + # }, + # { + # "provider": "openrouter", + # "model": "google/gemini-2.5-flash", + # "max_retries": 3, + # "temperature": 0.2, + # "max_tokens": 16000, + # "timeout": 300.0, + # }, ], - "run_organization_enrichment": [ + "run_epfl_assessment": [ + { + "provider": "openai-compatible", + "model": "openai/gpt-oss-120b", + "base_url": "https://inference.rcp.epfl.ch/v1", + "api_key_env": "RCP_TOKEN", + "max_retries": 2, + "temperature": 0.1, + "max_tokens": 8000, + "timeout": 300.0, + "allow_tools": False, # No tools needed for assessment + }, # { - # "provider": "openai-compatible", - # "model": "openai/gpt-oss-120b", - # "base_url": "https://inference.rcp.epfl.ch/v1", - # "api_key_env": "RCP_TOKEN", + # "provider": "openai", + # "model": "o4-mini", # "max_retries": 2, # "temperature": 0.1, # "max_tokens": 8000, # "timeout": 300.0, # }, + # { + # "provider": "openrouter", + # "model": "google/gemini-2.5-flash", + # "max_retries": 2, + # "temperature": 0.1, + # "max_tokens": 8000, + # "timeout": 300.0, + # }, + ], + "run_url_validation": [ { - "provider": "openai", - "model": "o4-mini", + "provider": "openai-compatible", + "model": "openai/gpt-oss-120b", + "base_url": "https://inference.rcp.epfl.ch/v1", + "api_key_env": "RCP_TOKEN", "max_retries": 2, "temperature": 0.1, - "max_tokens": 8000, - "timeout": 300.0, + "max_tokens": 4000, + "timeout": 60.0, + "allow_tools": False, # No tools needed for validation }, + # { + # "provider": "openai", + # "model": "gpt-4o-mini", + # "max_retries": 2, + # "temperature": 0.1, + # "max_tokens": 4000, + # "timeout": 60.0, + # }, + # { + # "provider": "openrouter", + # "model": "google/gemini-2.0-flash", + # "max_retries": 2, + # "temperature": 0.1, + # "max_tokens": 4000, + # "timeout": 60.0, + # }, + ], + "run_context_compiler": [ { - "provider": "openrouter", - "model": "google/gemini-2.5-flash", + "provider": "openai-compatible", + "model": "openai/gpt-oss-120b", + "base_url": "https://inference.rcp.epfl.ch/v1", + "api_key_env": "RCP_TOKEN", "max_retries": 3, "temperature": 0.2, "max_tokens": 16000, - "timeout": 300.0, + "timeout": 600.0, + "allow_tools": False, # No tools - only use repository content and GIMIE data }, ], - "run_academic_catalog_enrichment": [ + "run_structured_output": [ + { + "provider": "openai-compatible", + "model": "openai/gpt-oss-120b", + "base_url": "https://inference.rcp.epfl.ch/v1", + "api_key_env": "RCP_TOKEN", + "max_retries": 3, + "temperature": 0.2, + "max_tokens": 16000, + "timeout": 600.0, + "allow_tools": False, # No tools for structured output + }, # { - # "provider": "openai-compatible", - # "model": "openai/gpt-oss-120b", - # "base_url": "https://inference.rcp.epfl.ch/v1", - # "api_key_env": "RCP_TOKEN", + # "provider": "openai", + # "model": "o4-mini", # "max_retries": 3, - # "temperature": 0.1, - # "max_tokens": 12000, - # "timeout": 300.0, + # "temperature": 0.2, + # "max_tokens": 16000, + # "timeout": 600.0, # }, + # { + # "provider": "openrouter", + # "model": "google/gemini-2.5-flash", + # "max_retries": 3, + # "temperature": 0.2, + # "max_tokens": 16000, + # "timeout": 600.0, + # }, + ], + "run_repository_classifier": [ { - "provider": "openai", - "model": "o4-mini", - "max_retries": 3, + "provider": "openai-compatible", + "model": "openai/gpt-oss-120b", + "base_url": "https://inference.rcp.epfl.ch/v1", + "api_key_env": "RCP_TOKEN", + "max_retries": 2, "temperature": 0.1, - "max_tokens": 12000, + "max_tokens": 8000, "timeout": 300.0, + "allow_tools": False, # No tools - classifies from compiled context }, + # { + # "provider": "openai", + # "model": "o4-mini", + # "max_retries": 2, + # "temperature": 0.1, + # "max_tokens": 8000, + # "timeout": 300.0, + # }, + # { + # "provider": "openrouter", + # "model": "google/gemini-2.5-flash", + # "max_retries": 2, + # "temperature": 0.1, + # "max_tokens": 8000, + # "timeout": 300.0, + # }, + ], + "run_organization_identifier": [ { - "provider": "openrouter", - "model": "google/gemini-2.5-flash", - "max_retries": 3, - "temperature": 0.2, - "max_tokens": 16000, + "provider": "openai-compatible", + "model": "openai/gpt-oss-120b", + "base_url": "https://inference.rcp.epfl.ch/v1", + "api_key_env": "RCP_TOKEN", + "max_retries": 2, + "temperature": 0.1, + "max_tokens": 8000, "timeout": 300.0, + "allow_tools": False, # No tools - identifies from compiled context }, - ], - "run_epfl_assessment": [ # { - # "provider": "openai-compatible", - # "model": "openai/gpt-oss-120b", - # "base_url": "https://inference.rcp.epfl.ch/v1", - # "api_key_env": "RCP_TOKEN", + # "provider": "openai", + # "model": "o4-mini", # "max_retries": 2, # "temperature": 0.1, # "max_tokens": 8000, # "timeout": 300.0, # }, + # { + # "provider": "openrouter", + # "model": "google/gemini-2.5-flash", + # "max_retries": 2, + # "temperature": 0.1, + # "max_tokens": 8000, + # "timeout": 300.0, + # }, + ], + "run_epfl_final_checker": [ { - "provider": "openai", - "model": "o4-mini", + "provider": "openai-compatible", + "model": "openai/gpt-oss-120b", + "base_url": "https://inference.rcp.epfl.ch/v1", + "api_key_env": "RCP_TOKEN", "max_retries": 2, "temperature": 0.1, - "max_tokens": 8000, + "max_tokens": 16000, "timeout": 300.0, + "allow_tools": False, # No tools - analyzes enriched data only }, + ], + "run_linked_entities_searcher": [ { - "provider": "openrouter", - "model": "google/gemini-2.5-flash", + "provider": "openai-compatible", + "model": "openai/gpt-oss-120b", + "base_url": "https://inference.rcp.epfl.ch/v1", + "api_key_env": "RCP_TOKEN", "max_retries": 2, "temperature": 0.1, + "max_tokens": 12000, + "timeout": 400.0, + "allow_tools": True, # Needs Infoscience search tools + }, + ], + "run_user_context_compiler": [ + { + "provider": "openai-compatible", + "model": "openai/gpt-oss-120b", + "base_url": "https://inference.rcp.epfl.ch/v1", + "api_key_env": "RCP_TOKEN", + "max_retries": 3, + "temperature": 0.2, + "max_tokens": 16000, + "timeout": 600.0, + "allow_tools": True, # Uses ORCID, Infoscience, web search tools + }, + ], + "run_user_structured_output": [ + { + "provider": "openai-compatible", + "model": "openai/gpt-oss-120b", + "base_url": "https://inference.rcp.epfl.ch/v1", + "api_key_env": "RCP_TOKEN", + "max_retries": 3, + "temperature": 0.2, "max_tokens": 8000, "timeout": 300.0, + "allow_tools": False, # No tools for structured output }, + # { + # "provider": "openai", + # "model": "o4-mini", + # "max_retries": 3, + # "temperature": 0.2, + # "max_tokens": 8000, + # "timeout": 300.0, + # }, + # { + # "provider": "openrouter", + # "model": "google/gemini-2.5-flash", + # "max_retries": 3, + # "temperature": 0.2, + # "max_tokens": 16000, + # "timeout": 300.0, + # }, ], - "run_url_validation": [ + "run_user_classifier": [ { - "provider": "openai", - "model": "gpt-4o-mini", + "provider": "openai-compatible", + "model": "openai/gpt-oss-120b", + "base_url": "https://inference.rcp.epfl.ch/v1", + "api_key_env": "RCP_TOKEN", "max_retries": 2, "temperature": 0.1, "max_tokens": 4000, - "timeout": 60.0, + "timeout": 300.0, + "allow_tools": False, # No tools - classifies from compiled context + }, + # { + # "provider": "openai", + # "model": "o4-mini", + # "max_retries": 2, + # "temperature": 0.1, + # "max_tokens": 4000, + # "timeout": 300.0, + # }, + # { + # "provider": "openrouter", + # "model": "google/gemini-2.5-flash", + # "max_retries": 2, + # "temperature": 0.1, + # "max_tokens": 8000, + # "timeout": 300.0, + # }, + ], + "run_organization_context_compiler": [ + { + "provider": "openai-compatible", + "model": "openai/gpt-oss-120b", + "base_url": "https://inference.rcp.epfl.ch/v1", + "api_key_env": "RCP_TOKEN", + "max_retries": 3, + "temperature": 0.2, + "max_tokens": 16000, + "timeout": 600.0, + "allow_tools": True, # Uses Infoscience, web search tools }, + ], + "run_organization_structured_output": [ { - "provider": "openrouter", - "model": "google/gemini-2.0-flash", + "provider": "openai-compatible", + "model": "openai/gpt-oss-120b", + "base_url": "https://inference.rcp.epfl.ch/v1", + "api_key_env": "RCP_TOKEN", + "max_retries": 3, + "temperature": 0.2, + "max_tokens": 8000, + "timeout": 300.0, + "allow_tools": False, # No tools for structured output + }, + # { + # "provider": "openai", + # "model": "o4-mini", + # "max_retries": 3, + # "temperature": 0.2, + # "max_tokens": 8000, + # "timeout": 300.0, + # }, + # { + # "provider": "openrouter", + # "model": "google/gemini-2.5-flash", + # "max_retries": 3, + # "temperature": 0.2, + # "max_tokens": 16000, + # "timeout": 300.0, + # }, + ], + "run_organization_classifier": [ + { + "provider": "openai-compatible", + "model": "openai/gpt-oss-120b", + "base_url": "https://inference.rcp.epfl.ch/v1", + "api_key_env": "RCP_TOKEN", "max_retries": 2, "temperature": 0.1, "max_tokens": 4000, - "timeout": 60.0, + "timeout": 300.0, + "allow_tools": False, # No tools - classifies from compiled context }, + # { + # "provider": "openai", + # "model": "o4-mini", + # "max_retries": 2, + # "temperature": 0.1, + # "max_tokens": 4000, + # "timeout": 300.0, + # }, + # { + # "provider": "openrouter", + # "model": "google/gemini-2.5-flash", + # "max_retries": 2, + # "temperature": 0.1, + # "max_tokens": 8000, + # "timeout": 300.0, + # }, ], } @@ -187,9 +469,21 @@ "run_llm_analysis": "LLM_ANALYSIS_MODELS", "run_user_enrichment": "USER_ENRICHMENT_MODELS", "run_organization_enrichment": "ORG_ENRICHMENT_MODELS", - "run_academic_catalog_enrichment": "ACADEMIC_CATALOG_ENRICHMENT_MODELS", + "run_linked_entities_enrichment": "linked_entities_ENRICHMENT_MODELS", "run_epfl_assessment": "EPFL_ASSESSMENT_MODELS", + "run_repository_classifier": "REPOSITORY_CLASSIFIER_MODELS", + "run_organization_identifier": "ORGANIZATION_IDENTIFIER_MODELS", "run_url_validation": "URL_VALIDATION_MODELS", + "run_context_compiler": "CONTEXT_COMPILER_MODELS", + "run_structured_output": "STRUCTURED_OUTPUT_MODELS", + "run_epfl_final_checker": "EPFL_FINAL_CHECKER_MODELS", + "run_linked_entities_searcher": "LINKED_ENTITIES_SEARCHER_MODELS", + "run_user_context_compiler": "USER_CONTEXT_COMPILER_MODELS", + "run_user_structured_output": "USER_STRUCTURED_OUTPUT_MODELS", + "run_user_classifier": "USER_CLASSIFIER_MODELS", + "run_organization_context_compiler": "ORGANIZATION_CONTEXT_COMPILER_MODELS", + "run_organization_structured_output": "ORGANIZATION_STRUCTURED_OUTPUT_MODELS", + "run_organization_classifier": "ORGANIZATION_CLASSIFIER_MODELS", } @@ -292,6 +586,7 @@ def get_model_parameters(config: Dict[str, Any]) -> Dict[str, Any]: "base_url", "api_key_env", "max_completion_tokens", + "allow_tools", # Tool access flag, not a model parameter } # Filter out non-parameter keys diff --git a/src/parsers/users_parser.py b/src/parsers/users_parser.py index 944b006..35dd5bd 100644 --- a/src/parsers/users_parser.py +++ b/src/parsers/users_parser.py @@ -87,6 +87,9 @@ def get_user_metadata(self, username: str) -> GitHubUserMetadata: if orcid: orcid_activities = self._scrape_orcid_activities(orcid) + # Get repositories + repositories = self._get_user_repositories(username) + # Combine all data and create Pydantic model user_data = { "login": rest_data["login"], @@ -111,10 +114,38 @@ def get_user_metadata(self, username: str) -> GitHubUserMetadata: "social_accounts": graphql_data.get("social_accounts", []), "readme_url": readme_data.get("url"), "readme_content": readme_data.get("content"), + "repositories": repositories, } return GitHubUserMetadata(**user_data) + def _get_user_repositories(self, username: str, limit: int = 100) -> List[str]: + """ + Get user's repositories (limited for performance) + + Args: + username: GitHub username + limit: Maximum number of repositories to fetch (default 100) + + Returns: + List of repository names + """ + url = f"{self.rest_base_url}/users/{username}/repos" + params = {"per_page": limit, "sort": "updated"} + + try: + response = requests.get(url, headers=self.headers, params=params) + + if response.status_code != 200: + print(f"Warning: Failed to fetch repositories for {username}: {response.status_code}") + return [] + + repos_data = response.json() + return [repo["name"] for repo in repos_data] + except Exception as e: + print(f"Warning: Error fetching repositories for {username}: {e}") + return [] + def _scrape_orcid_from_profile(self, username: str) -> Optional[str]: """ Scrape ORCID from GitHub profile page diff --git a/src/utils/enhanced_logging.py b/src/utils/enhanced_logging.py index b927467..427618a 100644 --- a/src/utils/enhanced_logging.py +++ b/src/utils/enhanced_logging.py @@ -210,6 +210,12 @@ def setup_logging(level=logging.INFO, use_colors: bool = True): logging.getLogger("selenium").setLevel(logging.WARNING) logging.getLogger("httpx").setLevel(logging.WARNING) + # Silence noisy HTTP/API client loggers that log every request/response at DEBUG + logging.getLogger("httpcore").setLevel(logging.WARNING) + logging.getLogger("httpcore.http11").setLevel(logging.WARNING) + logging.getLogger("openai").setLevel(logging.WARNING) + logging.getLogger("openai._base_client").setLevel(logging.WARNING) + # Example usage context manager class RequestContext: diff --git a/src/utils/logging_config.py b/src/utils/logging_config.py index 6d02eb3..8a052b2 100644 --- a/src/utils/logging_config.py +++ b/src/utils/logging_config.py @@ -10,4 +10,12 @@ def setup_logging(level=logging.INFO): handlers=[logging.StreamHandler(sys.stdout)], ) + # Silence noisy external library loggers logging.getLogger("rdflib").setLevel(logging.WARNING) + + # Silence noisy HTTP/API client loggers that log every request/response at DEBUG + logging.getLogger("httpcore").setLevel(logging.WARNING) + logging.getLogger("httpcore.http11").setLevel(logging.WARNING) + logging.getLogger("httpx").setLevel(logging.WARNING) + logging.getLogger("openai").setLevel(logging.WARNING) + logging.getLogger("openai._base_client").setLevel(logging.WARNING) diff --git a/src/utils/token_counter.py b/src/utils/token_counter.py index 2b25749..434e6a2 100644 --- a/src/utils/token_counter.py +++ b/src/utils/token_counter.py @@ -119,3 +119,75 @@ def estimate_tokens_from_messages( if (input_tokens > 0 or output_tokens > 0) else None, } + + +def estimate_tokens_with_tools( + system_prompt: Optional[str] = None, + user_prompt: Optional[str] = None, + response: Optional[str] = None, + tool_calls: int = 0, + tool_results_text: Optional[str] = None, +) -> dict: + """ + Estimate token counts including tool call overhead. + + This function extends estimate_tokens_from_messages to account for: + - Tool call overhead (function definitions and call structures) + - Tool results text (returned data from tool executions) + - Multiple LLM round-trips when tools are used + + Args: + system_prompt: The system prompt sent to the model + user_prompt: The user prompt/query sent to the model + response: The model's response (final output after tool calls) + tool_calls: Number of tool calls made during the agent run + tool_results_text: Combined text of all tool results (optional, for more accurate estimation) + + Returns: + Dictionary with 'input_tokens', 'output_tokens', and 'total_tokens' estimates + """ + # Start with base estimation + base_estimate = estimate_tokens_from_messages( + system_prompt=system_prompt, + user_prompt=user_prompt, + response=response, + ) + + input_tokens = base_estimate.get("input_tokens", 0) or 0 + output_tokens = base_estimate.get("output_tokens", 0) or 0 + + # Add tool call overhead + if tool_calls > 0: + # Each tool call adds overhead: + # - Function call structure: ~50 tokens + # - Function definition in system prompt: ~50 tokens (one-time, but we estimate per call) + # - Tool call formatting: ~20 tokens + tool_call_overhead = tool_calls * 100 # Conservative estimate per tool call + + # Tool calls are part of the model's output (the model generates the function call) + output_tokens += tool_call_overhead + + # Tool results become part of the input for the next request + if tool_results_text: + tool_results_tokens = count_tokens(tool_results_text) + if tool_results_tokens is not None: + # Add tool results as input tokens (they're sent back to the model) + input_tokens += tool_results_tokens + # Add formatting overhead for tool results (~20 tokens per result) + input_tokens += tool_calls * 20 + else: + # If we don't have tool results text, estimate based on tool calls + # Assume each tool result is ~200 tokens on average + estimated_tool_results = tool_calls * 200 + input_tokens += estimated_tool_results + input_tokens += tool_calls * 20 # Formatting overhead + + # Additional overhead for tool-related message formatting + # Each tool interaction adds ~10 tokens for message structure + input_tokens += tool_calls * 10 + + return { + "input_tokens": input_tokens if input_tokens > 0 else 0, + "output_tokens": output_tokens if output_tokens > 0 else 0, + "total_tokens": input_tokens + output_tokens, + } diff --git a/src/utils/url_validation.py b/src/utils/url_validation.py index 713e88f..ba18321 100644 --- a/src/utils/url_validation.py +++ b/src/utils/url_validation.py @@ -4,12 +4,89 @@ import logging import re -from typing import Any, Dict +from typing import Any, Dict, Optional from urllib.parse import urlparse logger = logging.getLogger(__name__) +ORCID_ID_PATTERN = re.compile(r"^\d{4}-\d{4}-\d{4}-\d{3}[\dX]$") +ORCID_URL_PATTERN = re.compile( + r"^https?://orcid\.org/(\d{4}-\d{4}-\d{4}-\d{3}[\dX])/?$", + flags=re.IGNORECASE, +) + + +def _is_valid_orcid_checksum(orcid_id: str) -> bool: + """ + Validate ORCID checksum using ISO 7064 MOD 11-2. + + Args: + orcid_id: ORCID identifier in canonical form (XXXX-XXXX-XXXX-XXXX) + + Returns: + True if checksum is valid, False otherwise + """ + digits = orcid_id.replace("-", "") + if len(digits) != 16: + return False + + total = 0 + for char in digits[:15]: + if not char.isdigit(): + return False + total = (total + int(char)) * 2 + + remainder = total % 11 + result = (12 - remainder) % 11 + expected_check_digit = "X" if result == 10 else str(result) + + return digits[-1] == expected_check_digit + + +def normalize_orcid_id(orcid: Any) -> Optional[str]: + """ + Normalize ORCID to canonical ID form (XXXX-XXXX-XXXX-XXXX). + + Accepts both ID and URL input, validates pattern and checksum, and + returns None for invalid values. + """ + if not orcid: + return None + + if hasattr(orcid, "__str__"): + value = str(orcid).strip() + elif isinstance(orcid, str): + value = orcid.strip() + else: + return None + + if not value: + return None + + match = ORCID_URL_PATTERN.match(value) + if match: + candidate = match.group(1).upper() + else: + candidate = value.upper() + + if not ORCID_ID_PATTERN.match(candidate): + return None + + if not _is_valid_orcid_checksum(candidate): + return None + + return candidate + + +def normalize_orcid_url(orcid: Any) -> Optional[str]: + """Normalize ORCID input to canonical URL form.""" + normalized_id = normalize_orcid_id(orcid) + if not normalized_id: + return None + return f"https://orcid.org/{normalized_id}" + + def is_valid_url(url: Any) -> bool: """ Validate if a URL is properly formatted and accessible. @@ -99,27 +176,7 @@ def is_valid_orcid_url(url: Any) -> bool: Returns: True if valid ORCID URL, False otherwise """ - if not url: - return False - - # Handle Pydantic HttpUrl objects - if hasattr(url, "__str__"): - url = str(url) - elif not isinstance(url, str): - return False - - url = url.strip() - - # Check if it's a full ORCID URL - if url.startswith(("https://orcid.org/", "http://orcid.org/")): - orcid_id = url.replace("https://orcid.org/", "").replace( - "http://orcid.org/", - "", - ) - return bool(re.match(r"^\d{4}-\d{4}-\d{4}-\d{4}$", orcid_id)) - - # Check if it's just the ORCID ID - return bool(re.match(r"^\d{4}-\d{4}-\d{4}-\d{4}$", url)) + return normalize_orcid_id(url) is not None def is_valid_ror_url(url: Any) -> bool: @@ -237,7 +294,7 @@ def validate_and_clean_urls(data: Dict[str, Any]) -> Dict[str, Any]: if isinstance(url_value, str) and url_value.strip() == "": cleaned_data[field] = None continue - + if not is_valid_url(url_value): logger.warning(f"Invalid URL in {field}: {url_value!r}") cleaned_data[field] = None @@ -277,24 +334,15 @@ def validate_author_urls(author: Dict[str, Any]) -> Dict[str, Any]: """ cleaned_author = author.copy() - # Validate ORCID (validator in Person model handles format conversion) + # Validate ORCID and normalize to canonical ID format. if "orcid" in cleaned_author and cleaned_author["orcid"] is not None: - orcid = cleaned_author["orcid"] - - # Convert to string if needed - if hasattr(orcid, "__str__"): - orcid = str(orcid) - - if isinstance(orcid, str) and orcid.strip(): - orcid = orcid.strip() - # Basic validation - Person model validator will handle format conversion - if not (re.match(r"^\d{4}-\d{4}-\d{4}-\d{3}[\dX]$", orcid) or is_valid_orcid_url(orcid)): - logger.warning(f"Invalid ORCID format: {orcid}") - cleaned_author["orcid"] = None - else: - cleaned_author["orcid"] = orcid - else: + original_orcid = cleaned_author["orcid"] + normalized_orcid = normalize_orcid_id(original_orcid) + if normalized_orcid is None: + logger.warning(f"Invalid ORCID format: {original_orcid}") cleaned_author["orcid"] = None + else: + cleaned_author["orcid"] = normalized_orcid return cleaned_author diff --git a/src/utils/utils.py b/src/utils/utils.py index ef08801..074c3e3 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -10,8 +10,9 @@ from pydantic import BaseModel, HttpUrl, create_model from pyld import jsonld -from ..data_models import Person, SoftwareSourceCode +from ..data_models import Affiliation, Person, SoftwareSourceCode from ..parsers.users_parser import GitHubUsersParser +from .url_validation import normalize_orcid_id, normalize_orcid_url logger = logging.getLogger(__name__) @@ -250,17 +251,7 @@ def extract_orcid_id(orcid_url: str) -> Optional[str]: >>> extract_orcid_id("0000-0002-1126-1535") '0000-0002-1126-1535' """ - if not orcid_url: - return None - - # If it's already just the ID format, return it - orcid_pattern = r"\b(\d{4}-\d{4}-\d{4}-\d{3}[\dX])\b" - match = re.search(orcid_pattern, str(orcid_url)) - - if match: - return match.group(1) - - return None + return normalize_orcid_id(orcid_url) def normalize_orcid_to_url(orcid_input: str) -> Optional[str]: @@ -279,40 +270,25 @@ def normalize_orcid_to_url(orcid_input: str) -> Optional[str]: >>> normalize_orcid_to_url("https://orcid.org/0000-0002-1126-1535") 'https://orcid.org/0000-0002-1126-1535' """ - if not orcid_input: - return None - - # If it's already a URL, validate and return - if orcid_input.startswith("http"): - orcid_url_pattern = r"^https://orcid\.org/(\d{4}-\d{4}-\d{4}-\d{3}[\dX])$" - match = re.match(orcid_url_pattern, orcid_input) - if match: - return orcid_input - logger.warning(f"Invalid ORCID URL format: {orcid_input}") - return None + normalized = normalize_orcid_url(orcid_input) + if not normalized: + logger.warning(f"Invalid ORCID format: {orcid_input}") + return normalized - # If it's an ID, validate and convert to URL - orcid_id_pattern = r"^\d{4}-\d{4}-\d{4}-\d{3}[\dX]$" - if re.match(orcid_id_pattern, orcid_input): - return f"https://orcid.org/{orcid_input}" - logger.warning(f"Invalid ORCID format: {orcid_input}") - return None - - -def get_orcid_affiliations(orcid_id: str) -> List[str]: +def get_orcid_affiliations(orcid_id: str) -> List[Affiliation]: """ - Fetch affiliations (organization names only) from ORCID. + Fetch affiliations from ORCID with provenance tracking. Args: orcid_id: ORCID identifier (e.g., "0000-0002-1126-1535") Returns: - List of organization names from ORCID employment history + List of Affiliation objects from ORCID employment history Examples: >>> get_orcid_affiliations("0000-0002-1126-1535") - ['EPFL - École Polytechnique Fédérale de Lausanne', 'Swiss Data Science Center'] + [Affiliation(name='EPFL - École Polytechnique Fédérale de Lausanne', organizationId=None, source='orcid')] """ if not orcid_id: @@ -332,7 +308,7 @@ def fetch_affiliations(): if not orcid_activities or not orcid_activities.employment: return [] - # Extract only organization names, removing duplicates while preserving order + # Extract organization names and create Affiliation objects affiliations = [] seen = set() @@ -343,7 +319,13 @@ def fetch_affiliations(): org_name = org_name.split(":")[0].strip() if org_name and org_name not in seen: - affiliations.append(org_name) + affiliations.append( + Affiliation( + name=org_name, + organizationId=None, # ORCID doesn't provide ROR IDs directly + source="orcid", + ), + ) seen.add(org_name) return affiliations @@ -407,34 +389,25 @@ def enrich_author_with_orcid(author: Person) -> Person: return author logger.info( - f"Found {len(orcid_affiliations)} ORCID affiliations for {orcid_id}: {orcid_affiliations}", + f"Found {len(orcid_affiliations)} ORCID affiliations for {orcid_id}: {[aff.name for aff in orcid_affiliations]}", ) - # Get existing affiliations + # Merge affiliations by name (case-insensitive) existing_affiliations = author.affiliations or [] + existing_names = {aff.name.lower(): aff for aff in existing_affiliations} - # Ensure it's a list - if not isinstance(existing_affiliations, list): - existing_affiliations = [existing_affiliations] if existing_affiliations else [] - - # Merge affiliations, removing duplicates while preserving order - merged_affiliations = list(existing_affiliations) - seen = set(aff.lower() for aff in existing_affiliations) - - # Add ORCID affiliations that aren't already present added_count = 0 - for aff in orcid_affiliations: - if aff.lower() not in seen: - merged_affiliations.append(aff) - seen.add(aff.lower()) + for orcid_aff in orcid_affiliations: + if orcid_aff.name.lower() not in existing_names: + existing_affiliations.append(orcid_aff) added_count += 1 - author.affiliations = merged_affiliations + author.affiliations = existing_affiliations if added_count > 0: logger.info( f"Enriched author {author.name} with {added_count} new affiliations " - f"from ORCID (total: {len(merged_affiliations)})", + f"from ORCID (total: {len(author.affiliations)})", ) return author diff --git a/tests/test_cache.py b/tests/test_cache.py index a468fbb..7c6686f 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -4,18 +4,13 @@ This script shows how the caching system reduces external API calls. """ -import sys import time -from pathlib import Path import requests # Add src to path for imports -sys.path.insert(0, str(Path(__file__).parent / "src")) - -# Import after path modification -from core.cache import get_cache -from core.cache_manager import get_cache_manager +from src.cache.cache import get_cache +from src.cache.cache_manager import get_cache_manager def test_cache_basic_functionality(): diff --git a/tests/test_orcid_validation_pipeline.py b/tests/test_orcid_validation_pipeline.py new file mode 100644 index 0000000..ba9ae36 --- /dev/null +++ b/tests/test_orcid_validation_pipeline.py @@ -0,0 +1,269 @@ +""" +Regression tests for ORCID sanitization in repository analysis and JSON-LD conversion. +""" + +from datetime import datetime +import asyncio + +from fastapi import Response + +import src.api as api_module +import src.analysis.repositories as repositories_module +import src.cache.cache_manager as cache_manager_module +from src.analysis.repositories import Repository +from src.data_models.conversion import ( + convert_pydantic_to_jsonld, + create_simplified_model, +) +from src.data_models.models import Person +from src.data_models.repository import SoftwareSourceCode +from src.utils.url_validation import ( + normalize_orcid_id, + normalize_orcid_url, + validate_author_urls, +) + + +def _build_repository(tmp_path, monkeypatch) -> Repository: + monkeypatch.setenv("CACHE_DB_PATH", str(tmp_path / "cache.db")) + monkeypatch.setattr( + repositories_module, + "is_github_repo_public", + lambda _repo_url: True, + ) + cache_manager_module._cache_manager = None + repo = Repository("https://github.com/example/repo", force_refresh=False) + repo.gimie = None + return repo + + +def test_union_reconciliation_drops_invalid_orcid(tmp_path, monkeypatch): + repo = _build_repository(tmp_path, monkeypatch) + _, union_metadata = create_simplified_model( + SoftwareSourceCode, + field_filter=["author"], + ) + + simplified_dict = { + "authorPerson": [ + { + "name": "Bad ORCID Author", + "orcid": "0000-0009-0008-0143-9118", + "emails": ["bad.orcid@example.org"], + "affiliations": [], + }, + ], + "authorOrganization": None, + } + + full_dict = repo._convert_simplified_to_full( + simplified_dict=simplified_dict, + union_metadata=union_metadata, + git_authors=[], + ) + + assert "author" in full_dict + assert len(full_dict["author"]) == 1 + author = full_dict["author"][0] + assert isinstance(author, Person) + assert author.name == "Bad ORCID Author" + assert author.orcid is None + + +def test_orcid_normalization_and_jsonld_conversion_for_valid_values(): + valid_id = "0000-0002-1126-1535" + valid_url = "https://orcid.org/0000-0002-1126-1535" + + assert normalize_orcid_id(valid_id) == valid_id + assert normalize_orcid_id(valid_url) == valid_id + assert normalize_orcid_url(valid_id) == valid_url + + person = Person( + id="https://github.com/tester", + name="Tester", + orcid=valid_url, + ) + jsonld = convert_pydantic_to_jsonld(person) + graph = jsonld.get("@graph", []) + assert any( + node.get("md4i:orcidId", {}).get("@id") == valid_url + for node in graph + if isinstance(node, dict) + ) + + +def test_merge_person_objects_prefers_valid_orcid_when_mixed(tmp_path, monkeypatch): + repo = _build_repository(tmp_path, monkeypatch) + + valid_person = Person( + id="https://github.com/valid", + name="Robin Franken", + orcid="0000-0002-6441-8540", + ) + invalid_person = Person.model_construct( + id="https://github.com/invalid", + type="Person", + name="Robin Franken", + emails=[], + githubId="rmfranken", + orcid="0000-0009-0008-0143-9118", + affiliations=[], + affiliationHistory=[], + source="gimie", + linkedEntities=[], + ) + + merged = repo._merge_person_objects([valid_person, invalid_person]) + assert merged.orcid == "0000-0002-6441-8540" + + +def test_merge_person_objects_drops_only_invalid_orcid_values(tmp_path, monkeypatch): + repo = _build_repository(tmp_path, monkeypatch) + + invalid_one = Person.model_construct( + id="https://github.com/a", + type="Person", + name="Same Author", + emails=[], + githubId=None, + orcid="0000-0009-0008-0143-9118", + affiliations=[], + affiliationHistory=[], + source="gimie", + linkedEntities=[], + ) + invalid_two = Person.model_construct( + id="https://github.com/b", + type="Person", + name="Same Author", + emails=[], + githubId=None, + orcid="0000-0009-0008-0143-9118", + affiliations=[], + affiliationHistory=[], + source="gimie", + linkedEntities=[], + ) + + merged = repo._merge_person_objects([invalid_one, invalid_two]) + assert merged.orcid is None + + +def test_validate_author_urls_normalizes_orcid_and_drops_invalid(): + valid_author = validate_author_urls( + {"name": "Author A", "orcid": "https://orcid.org/0000-0002-1126-1535"}, + ) + invalid_author = validate_author_urls( + {"name": "Author B", "orcid": "0000-0009-0008-0143-9118"}, + ) + + assert valid_author["orcid"] == "0000-0002-1126-1535" + assert invalid_author["orcid"] is None + + +def test_llm_jsonld_endpoint_handles_malformed_orcid_without_500( + tmp_path, + monkeypatch, +): + monkeypatch.setenv("CACHE_DB_PATH", str(tmp_path / "cache.db")) + + class FakeRepository(repositories_module.Repository): + def __init__(self, full_path: str, force_refresh: bool = False): + self.full_path = full_path + self.force_refresh = force_refresh + self.data = None + self.gimie = None + + # Stats fields consumed by get_usage_stats() in the API endpoint. + self.total_input_tokens = 0 + self.total_output_tokens = 0 + self.estimated_input_tokens = 0 + self.estimated_output_tokens = 0 + self.start_time = datetime.now() + self.end_time = self.start_time + self.analysis_successful = True + + async def run_analysis( + self, + run_gimie: bool = True, + run_llm: bool = True, + run_user_enrichment: bool = False, + run_organization_enrichment: bool = False, + ): + # Build output through the real reconciliation path using malformed ORCID. + _, union_metadata = create_simplified_model( + SoftwareSourceCode, + field_filter=["author"], + ) + simplified_dict = { + "name": "orcid-regression-test", + "description": "Regression fixture", + "repositoryType": "software", + "repositoryTypeJustification": ["test fixture"], + "authorPerson": [ + { + "name": "Malformed ORCID Author", + "orcid": "0000-0009-0008-0143-9118", + "emails": ["bad.orcid@example.org"], + "affiliations": [], + }, + ], + "authorOrganization": None, + } + full_dict = self._convert_simplified_to_full( + simplified_dict=simplified_dict, + union_metadata=union_metadata, + git_authors=[], + ) + self.data = SoftwareSourceCode.model_validate(full_dict) + + def dump_results(self, output_type="json"): + return self.data.convert_pydantic_to_jsonld() + + def get_usage_stats(self): + return { + "input_tokens": self.total_input_tokens, + "output_tokens": self.total_output_tokens, + "total_tokens": self.total_input_tokens + self.total_output_tokens, + "estimated_input_tokens": self.estimated_input_tokens, + "estimated_output_tokens": self.estimated_output_tokens, + "estimated_total_tokens": self.estimated_input_tokens + + self.estimated_output_tokens, + "duration": 0.0, + "start_time": self.start_time, + "end_time": self.end_time, + "status_code": 200, + } + + def fake_validate_github_token(): + now = datetime.now() + return { + "valid": True, + "rate_limit_limit": 5000, + "rate_limit_remaining": 4999, + "rate_limit_reset": now, + } + + monkeypatch.setattr(api_module, "Repository", FakeRepository) + response = Response() + result = asyncio.run( + api_module.llm_jsonld( + response=response, + full_path="https://github.com/example/repo", + force_refresh=True, + enrich_orgs=False, + enrich_users=False, + github_info=fake_validate_github_token(), + ), + ) + + assert result.output is not None + assert "@graph" in result.output + + graph_nodes = result.output["@graph"] + malformed_orcid = "https://orcid.org/0000-0009-0008-0143-9118" + assert not any( + node.get("md4i:orcidId", {}).get("@id") == malformed_orcid + for node in graph_nodes + if isinstance(node, dict) + ) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..2fb2764 --- /dev/null +++ b/uv.lock @@ -0,0 +1,4288 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] + +[[package]] +name = "ag-ui-protocol" +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/bb/5a5ec893eea5805fb9a3db76a9888c3429710dfb6f24bbb37568f2cf7320/ag_ui_protocol-0.1.10.tar.gz", hash = "sha256:3213991c6b2eb24bb1a8c362ee270c16705a07a4c5962267a083d0959ed894f4", size = 6945, upload-time = "2025-11-06T15:17:17.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/78/eb55fabaab41abc53f52c0918a9a8c0f747807e5306273f51120fd695957/ag_ui_protocol-0.1.10-py3-none-any.whl", hash = "sha256:c81e6981f30aabdf97a7ee312bfd4df0cd38e718d9fc10019c7d438128b93ab5", size = 7889, upload-time = "2025-11-06T15:17:15.325Z" }, +] + +[[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, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.12.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/dc/ef9394bde9080128ad401ac7ede185267ed637df03b51f05d14d1c99ad67/aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc", size = 703921, upload-time = "2025-07-29T05:49:43.584Z" }, + { url = "https://files.pythonhosted.org/packages/8f/42/63fccfc3a7ed97eb6e1a71722396f409c46b60a0552d8a56d7aad74e0df5/aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af", size = 480288, upload-time = "2025-07-29T05:49:47.851Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a2/7b8a020549f66ea2a68129db6960a762d2393248f1994499f8ba9728bbed/aiohttp-3.12.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421", size = 468063, upload-time = "2025-07-29T05:49:49.789Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f5/d11e088da9176e2ad8220338ae0000ed5429a15f3c9dfd983f39105399cd/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79", size = 1650122, upload-time = "2025-07-29T05:49:51.874Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6b/b60ce2757e2faed3d70ed45dafee48cee7bfb878785a9423f7e883f0639c/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77", size = 1624176, upload-time = "2025-07-29T05:49:53.805Z" }, + { url = "https://files.pythonhosted.org/packages/dd/de/8c9fde2072a1b72c4fadecf4f7d4be7a85b1d9a4ab333d8245694057b4c6/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c", size = 1696583, upload-time = "2025-07-29T05:49:55.338Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ad/07f863ca3d895a1ad958a54006c6dafb4f9310f8c2fdb5f961b8529029d3/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4", size = 1738896, upload-time = "2025-07-29T05:49:57.045Z" }, + { url = "https://files.pythonhosted.org/packages/20/43/2bd482ebe2b126533e8755a49b128ec4e58f1a3af56879a3abdb7b42c54f/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6", size = 1643561, upload-time = "2025-07-29T05:49:58.762Z" }, + { url = "https://files.pythonhosted.org/packages/23/40/2fa9f514c4cf4cbae8d7911927f81a1901838baf5e09a8b2c299de1acfe5/aiohttp-3.12.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2", size = 1583685, upload-time = "2025-07-29T05:50:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c3/94dc7357bc421f4fb978ca72a201a6c604ee90148f1181790c129396ceeb/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d", size = 1627533, upload-time = "2025-07-29T05:50:02.306Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3f/1f8911fe1844a07001e26593b5c255a685318943864b27b4e0267e840f95/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb", size = 1638319, upload-time = "2025-07-29T05:50:04.282Z" }, + { url = "https://files.pythonhosted.org/packages/4e/46/27bf57a99168c4e145ffee6b63d0458b9c66e58bb70687c23ad3d2f0bd17/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5", size = 1613776, upload-time = "2025-07-29T05:50:05.863Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7e/1d2d9061a574584bb4ad3dbdba0da90a27fdc795bc227def3a46186a8bc1/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b", size = 1693359, upload-time = "2025-07-29T05:50:07.563Z" }, + { url = "https://files.pythonhosted.org/packages/08/98/bee429b52233c4a391980a5b3b196b060872a13eadd41c3a34be9b1469ed/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065", size = 1716598, upload-time = "2025-07-29T05:50:09.33Z" }, + { url = "https://files.pythonhosted.org/packages/57/39/b0314c1ea774df3392751b686104a3938c63ece2b7ce0ba1ed7c0b4a934f/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1", size = 1644940, upload-time = "2025-07-29T05:50:11.334Z" }, + { url = "https://files.pythonhosted.org/packages/1b/83/3dacb8d3f8f512c8ca43e3fa8a68b20583bd25636ffa4e56ee841ffd79ae/aiohttp-3.12.15-cp310-cp310-win32.whl", hash = "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a", size = 429239, upload-time = "2025-07-29T05:50:12.803Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f9/470b5daba04d558c9673ca2034f28d067f3202a40e17804425f0c331c89f/aiohttp-3.12.15-cp310-cp310-win_amd64.whl", hash = "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830", size = 452297, upload-time = "2025-07-29T05:50:14.266Z" }, + { url = "https://files.pythonhosted.org/packages/20/19/9e86722ec8e835959bd97ce8c1efa78cf361fa4531fca372551abcc9cdd6/aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117", size = 711246, upload-time = "2025-07-29T05:50:15.937Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/0a31fcb1a7d4629ac9d8f01f1cb9242e2f9943f47f5d03215af91c3c1a26/aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe", size = 483515, upload-time = "2025-07-29T05:50:17.442Z" }, + { url = "https://files.pythonhosted.org/packages/62/6c/94846f576f1d11df0c2e41d3001000527c0fdf63fce7e69b3927a731325d/aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9", size = 471776, upload-time = "2025-07-29T05:50:19.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6c/f766d0aaafcee0447fad0328da780d344489c042e25cd58fde566bf40aed/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5", size = 1741977, upload-time = "2025-07-29T05:50:21.665Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/fb779a05ba6ff44d7bc1e9d24c644e876bfff5abe5454f7b854cace1b9cc/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728", size = 1690645, upload-time = "2025-07-29T05:50:23.333Z" }, + { url = "https://files.pythonhosted.org/packages/37/4e/a22e799c2035f5d6a4ad2cf8e7c1d1bd0923192871dd6e367dafb158b14c/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16", size = 1789437, upload-time = "2025-07-29T05:50:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/28/e5/55a33b991f6433569babb56018b2fb8fb9146424f8b3a0c8ecca80556762/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0", size = 1828482, upload-time = "2025-07-29T05:50:26.693Z" }, + { url = "https://files.pythonhosted.org/packages/c6/82/1ddf0ea4f2f3afe79dffed5e8a246737cff6cbe781887a6a170299e33204/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b", size = 1730944, upload-time = "2025-07-29T05:50:28.382Z" }, + { url = "https://files.pythonhosted.org/packages/1b/96/784c785674117b4cb3877522a177ba1b5e4db9ce0fd519430b5de76eec90/aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd", size = 1668020, upload-time = "2025-07-29T05:50:30.032Z" }, + { url = "https://files.pythonhosted.org/packages/12/8a/8b75f203ea7e5c21c0920d84dd24a5c0e971fe1e9b9ebbf29ae7e8e39790/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8", size = 1716292, upload-time = "2025-07-29T05:50:31.983Z" }, + { url = "https://files.pythonhosted.org/packages/47/0b/a1451543475bb6b86a5cfc27861e52b14085ae232896a2654ff1231c0992/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50", size = 1711451, upload-time = "2025-07-29T05:50:33.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/fd/793a23a197cc2f0d29188805cfc93aa613407f07e5f9da5cd1366afd9d7c/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676", size = 1691634, upload-time = "2025-07-29T05:50:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bf/23a335a6670b5f5dfc6d268328e55a22651b440fca341a64fccf1eada0c6/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7", size = 1785238, upload-time = "2025-07-29T05:50:37.597Z" }, + { url = "https://files.pythonhosted.org/packages/57/4f/ed60a591839a9d85d40694aba5cef86dde9ee51ce6cca0bb30d6eb1581e7/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7", size = 1805701, upload-time = "2025-07-29T05:50:39.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/e0/444747a9455c5de188c0f4a0173ee701e2e325d4b2550e9af84abb20cdba/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685", size = 1718758, upload-time = "2025-07-29T05:50:41.292Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/1006278d1ffd13a698e5dd4bfa01e5878f6bddefc296c8b62649753ff249/aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b", size = 428868, upload-time = "2025-07-29T05:50:43.063Z" }, + { url = "https://files.pythonhosted.org/packages/10/97/ad2b18700708452400278039272032170246a1bf8ec5d832772372c71f1a/aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d", size = 453273, upload-time = "2025-07-29T05:50:44.613Z" }, + { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333, upload-time = "2025-07-29T05:50:46.507Z" }, + { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948, upload-time = "2025-07-29T05:50:48.067Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787, upload-time = "2025-07-29T05:50:49.669Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590, upload-time = "2025-07-29T05:50:51.368Z" }, + { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241, upload-time = "2025-07-29T05:50:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335, upload-time = "2025-07-29T05:50:55.394Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491, upload-time = "2025-07-29T05:50:57.202Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929, upload-time = "2025-07-29T05:50:59.192Z" }, + { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733, upload-time = "2025-07-29T05:51:01.394Z" }, + { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790, upload-time = "2025-07-29T05:51:03.657Z" }, + { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245, upload-time = "2025-07-29T05:51:05.911Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899, upload-time = "2025-07-29T05:51:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459, upload-time = "2025-07-29T05:51:09.56Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434, upload-time = "2025-07-29T05:51:11.423Z" }, + { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045, upload-time = "2025-07-29T05:51:13.689Z" }, + { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591, upload-time = "2025-07-29T05:51:15.452Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266, upload-time = "2025-07-29T05:51:17.239Z" }, + { url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741, upload-time = "2025-07-29T05:51:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407, upload-time = "2025-07-29T05:51:21.165Z" }, + { url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703, upload-time = "2025-07-29T05:51:22.948Z" }, + { url = "https://files.pythonhosted.org/packages/09/2f/d4bcc8448cf536b2b54eed48f19682031ad182faa3a3fee54ebe5b156387/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", size = 1705532, upload-time = "2025-07-29T05:51:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f3/59406396083f8b489261e3c011aa8aee9df360a96ac8fa5c2e7e1b8f0466/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", size = 1686794, upload-time = "2025-07-29T05:51:27.145Z" }, + { url = "https://files.pythonhosted.org/packages/dc/71/164d194993a8d114ee5656c3b7ae9c12ceee7040d076bf7b32fb98a8c5c6/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", size = 1738865, upload-time = "2025-07-29T05:51:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/1c/00/d198461b699188a93ead39cb458554d9f0f69879b95078dce416d3209b54/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", size = 1788238, upload-time = "2025-07-29T05:51:31.285Z" }, + { url = "https://files.pythonhosted.org/packages/85/b8/9e7175e1fa0ac8e56baa83bf3c214823ce250d0028955dfb23f43d5e61fd/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", size = 1710566, upload-time = "2025-07-29T05:51:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/59/e4/16a8eac9df39b48ae102ec030fa9f726d3570732e46ba0c592aeeb507b93/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", size = 1624270, upload-time = "2025-07-29T05:51:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/cd84dee7b6ace0740908fd0af170f9fab50c2a41ccbc3806aabcb1050141/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", size = 1677294, upload-time = "2025-07-29T05:51:37.215Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/d0f1f85e50d401eccd12bf85c46ba84f947a84839c8a1c2c5f6e8ab1eb50/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", size = 1708958, upload-time = "2025-07-29T05:51:39.328Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6b/f6fa6c5790fb602538483aa5a1b86fcbad66244997e5230d88f9412ef24c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", size = 1651553, upload-time = "2025-07-29T05:51:41.356Z" }, + { url = "https://files.pythonhosted.org/packages/04/36/a6d36ad545fa12e61d11d1932eef273928b0495e6a576eb2af04297fdd3c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", size = 1727688, upload-time = "2025-07-29T05:51:43.452Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c8/f195e5e06608a97a4e52c5d41c7927301bf757a8e8bb5bbf8cef6c314961/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", size = 1761157, upload-time = "2025-07-29T05:51:45.643Z" }, + { url = "https://files.pythonhosted.org/packages/05/6a/ea199e61b67f25ba688d3ce93f63b49b0a4e3b3d380f03971b4646412fc6/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", size = 1710050, upload-time = "2025-07-29T05:51:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2e/ffeb7f6256b33635c29dbed29a22a723ff2dd7401fff42ea60cf2060abfb/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", size = 422647, upload-time = "2025-07-29T05:51:50.718Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067, upload-time = "2025-07-29T05:51:52.549Z" }, +] + +[[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, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/07/61f3ca8e69c5dcdaec31b36b79a53ea21c5b4ca5e93c7df58c71f43bf8d8/anthropic-0.72.0.tar.gz", hash = "sha256:8971fe76dcffc644f74ac3883069beb1527641115ae0d6eb8fa21c1ce4082f7a", size = 493721, upload-time = "2025-10-28T19:13:01.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/b7/160d4fb30080395b4143f1d1a4f6c646ba9105561108d2a434b606c03579/anthropic-0.72.0-py3-none-any.whl", hash = "sha256:0e9f5a7582f038cab8efbb4c959e49ef654a56bfc7ba2da51b5a7b8a84de2e4d", size = 357464, upload-time = "2025-10-28T19:13:00.215Z" }, +] + +[[package]] +name = "anyio" +version = "4.11.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/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "argcomplete" +version = "3.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, +] + +[[package]] +name = "argparse" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/dd/e617cfc3f6210ae183374cd9f6a26b20514bbb5a792af97949c5aacddf0f/argparse-1.4.0.tar.gz", hash = "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4", size = 70508, upload-time = "2015-09-12T20:22:16.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/94/3af39d34be01a24a6e65433d19e107099374224905f1e0cc6bbe1fd22a2f/argparse-1.4.0-py2.py3-none-any.whl", hash = "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314", size = 23000, upload-time = "2015-09-14T16:03:16.137Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" }, +] + +[[package]] +name = "black" +version = "25.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/d2/6caccbc96f9311e8ec3378c296d4f4809429c43a6cd2394e3c390e86816d/black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e", size = 1743501, upload-time = "2025-11-10T01:59:06.202Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/b986d57828b3f3dccbf922e2864223197ba32e74c5004264b1c62bc9f04d/black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0", size = 1597308, upload-time = "2025-11-10T01:57:58.633Z" }, + { url = "https://files.pythonhosted.org/packages/39/8e/8b58ef4b37073f52b64a7b2dd8c9a96c84f45d6f47d878d0aa557e9a2d35/black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37", size = 1656194, upload-time = "2025-11-10T01:57:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/8d/30/9c2267a7955ecc545306534ab88923769a979ac20a27cf618d370091e5dd/black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03", size = 1347996, upload-time = "2025-11-10T01:57:22.391Z" }, + { url = "https://files.pythonhosted.org/packages/c4/62/d304786b75ab0c530b833a89ce7d997924579fb7484ecd9266394903e394/black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a", size = 1727891, upload-time = "2025-11-10T02:01:40.507Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/ffe8a006aa522c9e3f430e7b93568a7b2163f4b3f16e8feb6d8c3552761a/black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170", size = 1581875, upload-time = "2025-11-10T01:57:51.192Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7c8bda3108d0bb57387ac41b4abb5c08782b26da9f9c4421ef6694dac01a/black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc", size = 1642716, upload-time = "2025-11-10T01:56:51.589Z" }, + { url = "https://files.pythonhosted.org/packages/34/b9/f17dea34eecb7cc2609a89627d480fb6caea7b86190708eaa7eb15ed25e7/black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e", size = 1352904, upload-time = "2025-11-10T01:59:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/7f/12/5c35e600b515f35ffd737da7febdb2ab66bb8c24d88560d5e3ef3d28c3fd/black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac", size = 1772831, upload-time = "2025-11-10T02:03:47Z" }, + { url = "https://files.pythonhosted.org/packages/1a/75/b3896bec5a2bb9ed2f989a970ea40e7062f8936f95425879bbe162746fe5/black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96", size = 1608520, upload-time = "2025-11-10T01:58:46.895Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b5/2bfc18330eddbcfb5aab8d2d720663cd410f51b2ed01375f5be3751595b0/black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd", size = 1682719, upload-time = "2025-11-10T01:56:55.24Z" }, + { url = "https://files.pythonhosted.org/packages/96/fb/f7dc2793a22cdf74a72114b5ed77fe3349a2e09ef34565857a2f917abdf2/black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409", size = 1362684, upload-time = "2025-11-10T01:57:07.639Z" }, + { url = "https://files.pythonhosted.org/packages/ad/47/3378d6a2ddefe18553d1115e36aea98f4a90de53b6a3017ed861ba1bd3bc/black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b", size = 1772446, upload-time = "2025-11-10T02:02:16.181Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4b/0f00bfb3d1f7e05e25bfc7c363f54dc523bb6ba502f98f4ad3acf01ab2e4/black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd", size = 1607983, upload-time = "2025-11-10T02:02:52.502Z" }, + { url = "https://files.pythonhosted.org/packages/99/fe/49b0768f8c9ae57eb74cc10a1f87b4c70453551d8ad498959721cc345cb7/black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993", size = 1682481, upload-time = "2025-11-10T01:57:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/55/17/7e10ff1267bfa950cc16f0a411d457cdff79678fbb77a6c73b73a5317904/black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c", size = 1363869, upload-time = "2025-11-10T01:58:24.608Z" }, + { url = "https://files.pythonhosted.org/packages/67/c0/cc865ce594d09e4cd4dfca5e11994ebb51604328489f3ca3ae7bb38a7db5/black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170", size = 1771358, upload-time = "2025-11-10T02:03:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/37/77/4297114d9e2fd2fc8ab0ab87192643cd49409eb059e2940391e7d2340e57/black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545", size = 1612902, upload-time = "2025-11-10T01:59:33.382Z" }, + { url = "https://files.pythonhosted.org/packages/de/63/d45ef97ada84111e330b2b2d45e1dd163e90bd116f00ac55927fb6bf8adb/black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda", size = 1680571, upload-time = "2025-11-10T01:57:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4b/5604710d61cdff613584028b4cb4607e56e148801ed9b38ee7970799dab6/black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664", size = 1382599, upload-time = "2025-11-10T01:57:57.427Z" }, + { url = "https://files.pythonhosted.org/packages/00/5d/aed32636ed30a6e7f9efd6ad14e2a0b0d687ae7c8c7ec4e4a557174b895c/black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b", size = 204918, upload-time = "2025-11-10T01:53:48.917Z" }, +] + +[[package]] +name = "boto3" +version = "1.40.70" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/12/d5ac34e0536e1914dde28245f014a635056dde0427f6efa09f104d7999f4/boto3-1.40.70.tar.gz", hash = "sha256:191443707b391232ed15676bf6bba7e53caec1e71aafa12ccad2e825c5ee15cc", size = 111638, upload-time = "2025-11-10T20:29:15.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/cf/e24d08b37cd318754a8e94906c8b34b88676899aad1907ff6942311f13c4/boto3-1.40.70-py3-none-any.whl", hash = "sha256:e8c2f4f4cb36297270f1023ebe5b100333e0e88ab6457a9687d80143d2e15bf9", size = 139358, upload-time = "2025-11-10T20:29:13.512Z" }, +] + +[[package]] +name = "botocore" +version = "1.40.70" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/c1/8c4c199ae1663feee579a15861e34f10b29da11ae6ea0ad7b6a847ef3823/botocore-1.40.70.tar.gz", hash = "sha256:61b1f2cecd54d1b28a081116fa113b97bf4e17da57c62ae2c2751fe4c528af1f", size = 14444592, upload-time = "2025-11-10T20:29:04.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/d2/507fd0ee4dd574d2bdbdeac5df83f39d2cae1ffe97d4622cca6f6bab39f1/botocore-1.40.70-py3-none-any.whl", hash = "sha256:4a394ad25f5d9f1ef0bed610365744523eeb5c22de6862ab25d8c93f9f6d295c", size = 14106829, upload-time = "2025-11-10T20:29:01.101Z" }, +] + +[[package]] +name = "cachetools" +version = "6.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201", size = 31325, upload-time = "2025-10-12T14:55:30.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701", size = 11280, upload-time = "2025-10-12T14:55:28.382Z" }, +] + +[[package]] +name = "calamus" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lazy-object-proxy" }, + { name = "marshmallow" }, + { name = "pyld" }, + { name = "rdflib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/5f/08e59b277f7d57f4794a7b5bc05d0e46c8720cd61609c9f409a7938eda24/calamus-0.4.3.tar.gz", hash = "sha256:9e76df3d4f75a97586b8c3d9fe8d10d77a9d79000b14b1213f710516a4347e90", size = 21103, upload-time = "2025-01-08T08:21:39.085Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/52/d3d2b3bb7092be4f0f31dd73b4a08bd0c4a6141ca9d24dfc8c82e65ce756/calamus-0.4.3-py3-none-any.whl", hash = "sha256:c62ea4050d068a7a721b4e0e2e988cc86d850f1407f0d0eb85a4debb9f2fb51d", size = 22815, upload-time = "2025-01-08T08:21:35.94Z" }, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "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, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "cohere" +version = "5.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastavro" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "pydantic" }, + { name = "pydantic-core" }, + { name = "requests" }, + { name = "tokenizers" }, + { name = "types-requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/0e5dcfa9d111b82de4f3c7d83fbc92f478d229c8a004cc63c321fe44bb42/cohere-5.20.0.tar.gz", hash = "sha256:fb5ad5afa47447dd7eb090ad29bdb3a8181b0e758a3b03ba6ed8ca48d68d11a7", size = 168600, upload-time = "2025-10-24T20:24:05.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/5c/e312678fb4dff827c748980ec18918307d25e39ce006c84f7c6b32bc5641/cohere-5.20.0-py3-none-any.whl", hash = "sha256:a95f17ed22be3f978363703beb6008b55000ce0e85124ddb976fa5b688014fea", size = 303306, upload-time = "2025-10-24T20:24:04.237Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.11.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/59/9698d57a3b11704c7b89b21d69e9d23ecf80d538cabb536c8b63f4a12322/coverage-7.11.3.tar.gz", hash = "sha256:0f59387f5e6edbbffec2281affb71cdc85e0776c1745150a3ab9b6c1d016106b", size = 815210, upload-time = "2025-11-10T00:13:17.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/68/b53157115ef76d50d1d916d6240e5cd5b3c14dba8ba1b984632b8221fc2e/coverage-7.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0c986537abca9b064510f3fd104ba33e98d3036608c7f2f5537f869bc10e1ee5", size = 216377, upload-time = "2025-11-10T00:10:27.317Z" }, + { url = "https://files.pythonhosted.org/packages/14/c1/d2f9d8e37123fe6e7ab8afcaab8195f13bc84a8b2f449a533fd4812ac724/coverage-7.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28c5251b3ab1d23e66f1130ca0c419747edfbcb4690de19467cd616861507af7", size = 216892, upload-time = "2025-11-10T00:10:30.624Z" }, + { url = "https://files.pythonhosted.org/packages/83/73/18f05d8010149b650ed97ee5c9f7e4ae68c05c7d913391523281e41c2495/coverage-7.11.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4f2bb4ee8dd40f9b2a80bb4adb2aecece9480ba1fa60d9382e8c8e0bd558e2eb", size = 243650, upload-time = "2025-11-10T00:10:32.392Z" }, + { url = "https://files.pythonhosted.org/packages/63/3c/c0cbb296c0ecc6dcbd70f4b473fcd7fe4517bbef8b09f4326d78f38adb87/coverage-7.11.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e5f4bfac975a2138215a38bda599ef00162e4143541cf7dd186da10a7f8e69f1", size = 245478, upload-time = "2025-11-10T00:10:34.157Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9a/dad288cf9faa142a14e75e39dc646d968b93d74e15c83e9b13fd628f2cb3/coverage-7.11.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f4cbfff5cf01fa07464439a8510affc9df281535f41a1f5312fbd2b59b4ab5c", size = 247337, upload-time = "2025-11-10T00:10:35.655Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ba/f6148ebf5547b3502013175e41bf3107a4e34b7dd19f9793a6ce0e1cd61f/coverage-7.11.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:31663572f20bf3406d7ac00d6981c7bbbcec302539d26b5ac596ca499664de31", size = 244328, upload-time = "2025-11-10T00:10:37.459Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4d/b93784d0b593c5df89a0d48cbbd2d0963e0ca089eaf877405849792e46d3/coverage-7.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9799bd6a910961cb666196b8583ed0ee125fa225c6fdee2cbf00232b861f29d2", size = 245381, upload-time = "2025-11-10T00:10:39.229Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/6735bfd4f0f736d457642ee056a570d704c9d57fdcd5c91ea5d6b15c944e/coverage-7.11.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:097acc18bedf2c6e3144eaf09b5f6034926c3c9bb9e10574ffd0942717232507", size = 243390, upload-time = "2025-11-10T00:10:40.984Z" }, + { url = "https://files.pythonhosted.org/packages/db/3d/7ba68ed52d1873d450aefd8d2f5a353e67b421915cb6c174e4222c7b918c/coverage-7.11.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6f033dec603eea88204589175782290a038b436105a8f3637a81c4359df27832", size = 243654, upload-time = "2025-11-10T00:10:42.496Z" }, + { url = "https://files.pythonhosted.org/packages/14/26/be2720c4c7bf73c6591ae4ab503a7b5a31c7a60ced6dba855cfcb4a5af7e/coverage-7.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dd9ca2d44ed8018c90efb72f237a2a140325a4c3339971364d758e78b175f58e", size = 244272, upload-time = "2025-11-10T00:10:44.39Z" }, + { url = "https://files.pythonhosted.org/packages/90/20/086f5697780df146dbc0df4ae9b6db2b23ddf5aa550f977b2825137728e9/coverage-7.11.3-cp310-cp310-win32.whl", hash = "sha256:900580bc99c145e2561ea91a2d207e639171870d8a18756eb57db944a017d4bb", size = 218969, upload-time = "2025-11-10T00:10:45.863Z" }, + { url = "https://files.pythonhosted.org/packages/98/5c/cc6faba945ede5088156da7770e30d06c38b8591785ac99bcfb2074f9ef6/coverage-7.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:c8be5bfcdc7832011b2652db29ed7672ce9d353dd19bce5272ca33dbcf60aaa8", size = 219903, upload-time = "2025-11-10T00:10:47.676Z" }, + { url = "https://files.pythonhosted.org/packages/92/92/43a961c0f57b666d01c92bcd960c7f93677de5e4ee7ca722564ad6dee0fa/coverage-7.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:200bb89fd2a8a07780eafcdff6463104dec459f3c838d980455cfa84f5e5e6e1", size = 216504, upload-time = "2025-11-10T00:10:49.524Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5c/dbfc73329726aef26dbf7fefef81b8a2afd1789343a579ea6d99bf15d26e/coverage-7.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d264402fc179776d43e557e1ca4a7d953020d3ee95f7ec19cc2c9d769277f06", size = 217006, upload-time = "2025-11-10T00:10:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e0/878c84fb6661964bc435beb1e28c050650aa30e4c1cdc12341e298700bda/coverage-7.11.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:385977d94fc155f8731c895accdfcc3dd0d9dd9ef90d102969df95d3c637ab80", size = 247415, upload-time = "2025-11-10T00:10:52.805Z" }, + { url = "https://files.pythonhosted.org/packages/56/9e/0677e78b1e6a13527f39c4b39c767b351e256b333050539861c63f98bd61/coverage-7.11.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0542ddf6107adbd2592f29da9f59f5d9cff7947b5bb4f734805085c327dcffaa", size = 249332, upload-time = "2025-11-10T00:10:54.35Z" }, + { url = "https://files.pythonhosted.org/packages/54/90/25fc343e4ce35514262451456de0953bcae5b37dda248aed50ee51234cee/coverage-7.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d60bf4d7f886989ddf80e121a7f4d140d9eac91f1d2385ce8eb6bda93d563297", size = 251443, upload-time = "2025-11-10T00:10:55.832Z" }, + { url = "https://files.pythonhosted.org/packages/13/56/bc02bbc890fd8b155a64285c93e2ab38647486701ac9c980d457cdae857a/coverage-7.11.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0a3b6e32457535df0d41d2d895da46434706dd85dbaf53fbc0d3bd7d914b362", size = 247554, upload-time = "2025-11-10T00:10:57.829Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ab/0318888d091d799a82d788c1e8d8bd280f1d5c41662bbb6e11187efe33e8/coverage-7.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:876a3ee7fd2613eb79602e4cdb39deb6b28c186e76124c3f29e580099ec21a87", size = 249139, upload-time = "2025-11-10T00:10:59.465Z" }, + { url = "https://files.pythonhosted.org/packages/79/d8/3ee50929c4cd36fcfcc0f45d753337001001116c8a5b8dd18d27ea645737/coverage-7.11.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a730cd0824e8083989f304e97b3f884189efb48e2151e07f57e9e138ab104200", size = 247209, upload-time = "2025-11-10T00:11:01.432Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/3cf06e327401c293e60c962b4b8a2ceb7167c1a428a02be3adbd1d7c7e4c/coverage-7.11.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b5cd111d3ab7390be0c07ad839235d5ad54d2ca497b5f5db86896098a77180a4", size = 246936, upload-time = "2025-11-10T00:11:02.964Z" }, + { url = "https://files.pythonhosted.org/packages/99/0b/ffc03dc8f4083817900fd367110015ef4dd227b37284104a5eb5edc9c106/coverage-7.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:074e6a5cd38e06671580b4d872c1a67955d4e69639e4b04e87fc03b494c1f060", size = 247835, upload-time = "2025-11-10T00:11:04.405Z" }, + { url = "https://files.pythonhosted.org/packages/17/4d/dbe54609ee066553d0bcdcdf108b177c78dab836292bee43f96d6a5674d1/coverage-7.11.3-cp311-cp311-win32.whl", hash = "sha256:86d27d2dd7c7c5a44710565933c7dc9cd70e65ef97142e260d16d555667deef7", size = 218994, upload-time = "2025-11-10T00:11:05.966Z" }, + { url = "https://files.pythonhosted.org/packages/94/11/8e7155df53f99553ad8114054806c01a2c0b08f303ea7e38b9831652d83d/coverage-7.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:ca90ef33a152205fb6f2f0c1f3e55c50df4ef049bb0940ebba666edd4cdebc55", size = 219926, upload-time = "2025-11-10T00:11:07.936Z" }, + { url = "https://files.pythonhosted.org/packages/1f/93/bea91b6a9e35d89c89a1cd5824bc72e45151a9c2a9ca0b50d9e9a85e3ae3/coverage-7.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:56f909a40d68947ef726ce6a34eb38f0ed241ffbe55c5007c64e616663bcbafc", size = 218599, upload-time = "2025-11-10T00:11:09.578Z" }, + { url = "https://files.pythonhosted.org/packages/c2/39/af056ec7a27c487e25c7f6b6e51d2ee9821dba1863173ddf4dc2eebef4f7/coverage-7.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b771b59ac0dfb7f139f70c85b42717ef400a6790abb6475ebac1ecee8de782f", size = 216676, upload-time = "2025-11-10T00:11:11.566Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f8/21126d34b174d037b5d01bea39077725cbb9a0da94a95c5f96929c695433/coverage-7.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:603c4414125fc9ae9000f17912dcfd3d3eb677d4e360b85206539240c96ea76e", size = 217034, upload-time = "2025-11-10T00:11:13.12Z" }, + { url = "https://files.pythonhosted.org/packages/d5/3f/0fd35f35658cdd11f7686303214bd5908225838f374db47f9e457c8d6df8/coverage-7.11.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:77ffb3b7704eb7b9b3298a01fe4509cef70117a52d50bcba29cffc5f53dd326a", size = 248531, upload-time = "2025-11-10T00:11:15.023Z" }, + { url = "https://files.pythonhosted.org/packages/8f/59/0bfc5900fc15ce4fd186e092451de776bef244565c840c9c026fd50857e1/coverage-7.11.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4d4ca49f5ba432b0755ebb0fc3a56be944a19a16bb33802264bbc7311622c0d1", size = 251290, upload-time = "2025-11-10T00:11:16.628Z" }, + { url = "https://files.pythonhosted.org/packages/71/88/d5c184001fa2ac82edf1b8f2cd91894d2230d7c309e937c54c796176e35b/coverage-7.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05fd3fb6edff0c98874d752013588836f458261e5eba587afe4c547bba544afd", size = 252375, upload-time = "2025-11-10T00:11:18.249Z" }, + { url = "https://files.pythonhosted.org/packages/5c/29/f60af9f823bf62c7a00ce1ac88441b9a9a467e499493e5cc65028c8b8dd2/coverage-7.11.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e920567f8c3a3ce68ae5a42cf7c2dc4bb6cc389f18bff2235dd8c03fa405de5", size = 248946, upload-time = "2025-11-10T00:11:20.202Z" }, + { url = "https://files.pythonhosted.org/packages/67/16/4662790f3b1e03fce5280cad93fd18711c35980beb3c6f28dca41b5230c6/coverage-7.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4bec8c7160688bd5a34e65c82984b25409563134d63285d8943d0599efbc448e", size = 250310, upload-time = "2025-11-10T00:11:21.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/dd6c2e28308a83e5fc1ee602f8204bd3aa5af685c104cb54499230cf56db/coverage-7.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:adb9b7b42c802bd8cb3927de8c1c26368ce50c8fdaa83a9d8551384d77537044", size = 248461, upload-time = "2025-11-10T00:11:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/16/fe/b71af12be9f59dc9eb060688fa19a95bf3223f56c5af1e9861dfa2275d2c/coverage-7.11.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c8f563b245b4ddb591e99f28e3cd140b85f114b38b7f95b2e42542f0603eb7d7", size = 248039, upload-time = "2025-11-10T00:11:25.07Z" }, + { url = "https://files.pythonhosted.org/packages/11/b8/023b2003a2cd96bdf607afe03d9b96c763cab6d76e024abe4473707c4eb8/coverage-7.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e2a96fdc7643c9517a317553aca13b5cae9bad9a5f32f4654ce247ae4d321405", size = 249903, upload-time = "2025-11-10T00:11:26.992Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/5f1076311aa67b1fa4687a724cc044346380e90ce7d94fec09fd384aa5fd/coverage-7.11.3-cp312-cp312-win32.whl", hash = "sha256:e8feeb5e8705835f0622af0fe7ff8d5cb388948454647086494d6c41ec142c2e", size = 219201, upload-time = "2025-11-10T00:11:28.619Z" }, + { url = "https://files.pythonhosted.org/packages/4f/24/d21688f48fe9fcc778956680fd5aaf69f4e23b245b7c7a4755cbd421d25b/coverage-7.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:abb903ffe46bd319d99979cdba350ae7016759bb69f47882242f7b93f3356055", size = 220012, upload-time = "2025-11-10T00:11:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/4f/9e/d5eb508065f291456378aa9b16698b8417d87cb084c2b597f3beb00a8084/coverage-7.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:1451464fd855d9bd000c19b71bb7dafea9ab815741fb0bd9e813d9b671462d6f", size = 218652, upload-time = "2025-11-10T00:11:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f6/d8572c058211c7d976f24dab71999a565501fb5b3cdcb59cf782f19c4acb/coverage-7.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84b892e968164b7a0498ddc5746cdf4e985700b902128421bb5cec1080a6ee36", size = 216694, upload-time = "2025-11-10T00:11:34.296Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f6/b6f9764d90c0ce1bce8d995649fa307fff21f4727b8d950fa2843b7b0de5/coverage-7.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f761dbcf45e9416ec4698e1a7649248005f0064ce3523a47402d1bff4af2779e", size = 217065, upload-time = "2025-11-10T00:11:36.281Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8d/a12cb424063019fd077b5be474258a0ed8369b92b6d0058e673f0a945982/coverage-7.11.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1410bac9e98afd9623f53876fae7d8a5db9f5a0ac1c9e7c5188463cb4b3212e2", size = 248062, upload-time = "2025-11-10T00:11:37.903Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9c/dab1a4e8e75ce053d14259d3d7485d68528a662e286e184685ea49e71156/coverage-7.11.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:004cdcea3457c0ea3233622cd3464c1e32ebba9b41578421097402bee6461b63", size = 250657, upload-time = "2025-11-10T00:11:39.509Z" }, + { url = "https://files.pythonhosted.org/packages/3f/89/a14f256438324f33bae36f9a1a7137729bf26b0a43f5eda60b147ec7c8c7/coverage-7.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f067ada2c333609b52835ca4d4868645d3b63ac04fb2b9a658c55bba7f667d3", size = 251900, upload-time = "2025-11-10T00:11:41.372Z" }, + { url = "https://files.pythonhosted.org/packages/04/07/75b0d476eb349f1296486b1418b44f2d8780cc8db47493de3755e5340076/coverage-7.11.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07bc7745c945a6d95676953e86ba7cebb9f11de7773951c387f4c07dc76d03f5", size = 248254, upload-time = "2025-11-10T00:11:43.27Z" }, + { url = "https://files.pythonhosted.org/packages/5a/4b/0c486581fa72873489ca092c52792d008a17954aa352809a7cbe6cf0bf07/coverage-7.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bba7e4743e37484ae17d5c3b8eb1ce78b564cb91b7ace2e2182b25f0f764cb5", size = 250041, upload-time = "2025-11-10T00:11:45.274Z" }, + { url = "https://files.pythonhosted.org/packages/af/a3/0059dafb240ae3e3291f81b8de00e9c511d3dd41d687a227dd4b529be591/coverage-7.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbffc22d80d86fbe456af9abb17f7a7766e7b2101f7edaacc3535501691563f7", size = 248004, upload-time = "2025-11-10T00:11:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/83/93/967d9662b1eb8c7c46917dcc7e4c1875724ac3e73c3cb78e86d7a0ac719d/coverage-7.11.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0dba4da36730e384669e05b765a2c49f39514dd3012fcc0398dd66fba8d746d5", size = 247828, upload-time = "2025-11-10T00:11:48.563Z" }, + { url = "https://files.pythonhosted.org/packages/4c/1c/5077493c03215701e212767e470b794548d817dfc6247a4718832cc71fac/coverage-7.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae12fe90b00b71a71b69f513773310782ce01d5f58d2ceb2b7c595ab9d222094", size = 249588, upload-time = "2025-11-10T00:11:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a5/77f64de461016e7da3e05d7d07975c89756fe672753e4cf74417fc9b9052/coverage-7.11.3-cp313-cp313-win32.whl", hash = "sha256:12d821de7408292530b0d241468b698bce18dd12ecaf45316149f53877885f8c", size = 219223, upload-time = "2025-11-10T00:11:52.184Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1c/ec51a3c1a59d225b44bdd3a4d463135b3159a535c2686fac965b698524f4/coverage-7.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:6bb599052a974bb6cedfa114f9778fedfad66854107cf81397ec87cb9b8fbcf2", size = 220033, upload-time = "2025-11-10T00:11:53.871Z" }, + { url = "https://files.pythonhosted.org/packages/01/ec/e0ce39746ed558564c16f2cc25fa95ce6fc9fa8bfb3b9e62855d4386b886/coverage-7.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:bb9d7efdb063903b3fdf77caec7b77c3066885068bdc0d44bc1b0c171033f944", size = 218661, upload-time = "2025-11-10T00:11:55.597Z" }, + { url = "https://files.pythonhosted.org/packages/46/cb/483f130bc56cbbad2638248915d97b185374d58b19e3cc3107359715949f/coverage-7.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fb58da65e3339b3dbe266b607bb936efb983d86b00b03eb04c4ad5b442c58428", size = 217389, upload-time = "2025-11-10T00:11:57.59Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ae/81f89bae3afef75553cf10e62feb57551535d16fd5859b9ee5a2a97ddd27/coverage-7.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d16bbe566e16a71d123cd66382c1315fcd520c7573652a8074a8fe281b38c6a", size = 217742, upload-time = "2025-11-10T00:11:59.519Z" }, + { url = "https://files.pythonhosted.org/packages/db/6e/a0fb897041949888191a49c36afd5c6f5d9f5fd757e0b0cd99ec198a324b/coverage-7.11.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8258f10059b5ac837232c589a350a2df4a96406d6d5f2a09ec587cbdd539655", size = 259049, upload-time = "2025-11-10T00:12:01.592Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/d13acc67eb402d91eb94b9bd60593411799aed09ce176ee8d8c0e39c94ca/coverage-7.11.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c5627429f7fbff4f4131cfdd6abd530734ef7761116811a707b88b7e205afd7", size = 261113, upload-time = "2025-11-10T00:12:03.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/07/a6868893c48191d60406df4356aa7f0f74e6de34ef1f03af0d49183e0fa1/coverage-7.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:465695268414e149bab754c54b0c45c8ceda73dd4a5c3ba255500da13984b16d", size = 263546, upload-time = "2025-11-10T00:12:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/e5/28598f70b2c1098332bac47925806353b3313511d984841111e6e760c016/coverage-7.11.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ebcddfcdfb4c614233cff6e9a3967a09484114a8b2e4f2c7a62dc83676ba13f", size = 258260, upload-time = "2025-11-10T00:12:07.137Z" }, + { url = "https://files.pythonhosted.org/packages/0e/58/58e2d9e6455a4ed746a480c4b9cf96dc3cb2a6b8f3efbee5efd33ae24b06/coverage-7.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13b2066303a1c1833c654d2af0455bb009b6e1727b3883c9964bc5c2f643c1d0", size = 261121, upload-time = "2025-11-10T00:12:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/17/57/38803eefb9b0409934cbc5a14e3978f0c85cb251d2b6f6a369067a7105a0/coverage-7.11.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d8750dd20362a1b80e3cf84f58013d4672f89663aee457ea59336df50fab6739", size = 258736, upload-time = "2025-11-10T00:12:11.195Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/f94683167156e93677b3442be1d4ca70cb33718df32a2eea44a5898f04f6/coverage-7.11.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ab6212e62ea0e1006531a2234e209607f360d98d18d532c2fa8e403c1afbdd71", size = 257625, upload-time = "2025-11-10T00:12:12.843Z" }, + { url = "https://files.pythonhosted.org/packages/87/ed/42d0bf1bc6bfa7d65f52299a31daaa866b4c11000855d753857fe78260ac/coverage-7.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b17c2b5e0b9bb7702449200f93e2d04cb04b1414c41424c08aa1e5d352da76", size = 259827, upload-time = "2025-11-10T00:12:15.128Z" }, + { url = "https://files.pythonhosted.org/packages/d3/76/5682719f5d5fbedb0c624c9851ef847407cae23362deb941f185f489c54e/coverage-7.11.3-cp313-cp313t-win32.whl", hash = "sha256:426559f105f644b69290ea414e154a0d320c3ad8a2bb75e62884731f69cf8e2c", size = 219897, upload-time = "2025-11-10T00:12:17.274Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/1da511d0ac3d39e6676fa6cc5ec35320bbf1cebb9b24e9ee7548ee4e931a/coverage-7.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:90a96fcd824564eae6137ec2563bd061d49a32944858d4bdbae5c00fb10e76ac", size = 220959, upload-time = "2025-11-10T00:12:19.292Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9d/e255da6a04e9ec5f7b633c54c0fdfa221a9e03550b67a9c83217de12e96c/coverage-7.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:1e33d0bebf895c7a0905fcfaff2b07ab900885fc78bba2a12291a2cfbab014cc", size = 219234, upload-time = "2025-11-10T00:12:21.251Z" }, + { url = "https://files.pythonhosted.org/packages/84/d6/634ec396e45aded1772dccf6c236e3e7c9604bc47b816e928f32ce7987d1/coverage-7.11.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fdc5255eb4815babcdf236fa1a806ccb546724c8a9b129fd1ea4a5448a0bf07c", size = 216746, upload-time = "2025-11-10T00:12:23.089Z" }, + { url = "https://files.pythonhosted.org/packages/28/76/1079547f9d46f9c7c7d0dad35b6873c98bc5aa721eeabceafabd722cd5e7/coverage-7.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fe3425dc6021f906c6325d3c415e048e7cdb955505a94f1eb774dafc779ba203", size = 217077, upload-time = "2025-11-10T00:12:24.863Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/6ad80d6ae0d7cb743b9a98df8bb88b1ff3dc54491508a4a97549c2b83400/coverage-7.11.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4ca5f876bf41b24378ee67c41d688155f0e54cdc720de8ef9ad6544005899240", size = 248122, upload-time = "2025-11-10T00:12:26.553Z" }, + { url = "https://files.pythonhosted.org/packages/20/1d/784b87270784b0b88e4beec9d028e8d58f73ae248032579c63ad2ac6f69a/coverage-7.11.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9061a3e3c92b27fd8036dafa26f25d95695b6aa2e4514ab16a254f297e664f83", size = 250638, upload-time = "2025-11-10T00:12:28.555Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/b6dd31e23e004e9de84d1a8672cd3d73e50f5dae65dbd0f03fa2cdde6100/coverage-7.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abcea3b5f0dc44e1d01c27090bc32ce6ffb7aa665f884f1890710454113ea902", size = 251972, upload-time = "2025-11-10T00:12:30.246Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ef/f9c64d76faac56b82daa036b34d4fe9ab55eb37f22062e68e9470583e688/coverage-7.11.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:68c4eb92997dbaaf839ea13527be463178ac0ddd37a7ac636b8bc11a51af2428", size = 248147, upload-time = "2025-11-10T00:12:32.195Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/5b666f90a8f8053bd264a1ce693d2edef2368e518afe70680070fca13ecd/coverage-7.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:149eccc85d48c8f06547534068c41d69a1a35322deaa4d69ba1561e2e9127e75", size = 249995, upload-time = "2025-11-10T00:12:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/eb/7b/871e991ffb5d067f8e67ffb635dabba65b231d6e0eb724a4a558f4a702a5/coverage-7.11.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:08c0bcf932e47795c49f0406054824b9d45671362dfc4269e0bc6e4bff010704", size = 247948, upload-time = "2025-11-10T00:12:36.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/ce454f0af9609431b06dbe5485fc9d1c35ddc387e32ae8e374f49005748b/coverage-7.11.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:39764c6167c82d68a2d8c97c33dba45ec0ad9172570860e12191416f4f8e6e1b", size = 247770, upload-time = "2025-11-10T00:12:38.167Z" }, + { url = "https://files.pythonhosted.org/packages/61/8f/79002cb58a61dfbd2085de7d0a46311ef2476823e7938db80284cedd2428/coverage-7.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3224c7baf34e923ffc78cb45e793925539d640d42c96646db62dbd61bbcfa131", size = 249431, upload-time = "2025-11-10T00:12:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/58/cc/d06685dae97468ed22999440f2f2f5060940ab0e7952a7295f236d98cce7/coverage-7.11.3-cp314-cp314-win32.whl", hash = "sha256:c713c1c528284d636cd37723b0b4c35c11190da6f932794e145fc40f8210a14a", size = 219508, upload-time = "2025-11-10T00:12:42.231Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ed/770cd07706a3598c545f62d75adf2e5bd3791bffccdcf708ec383ad42559/coverage-7.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:c381a252317f63ca0179d2c7918e83b99a4ff3101e1b24849b999a00f9cd4f86", size = 220325, upload-time = "2025-11-10T00:12:44.065Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ac/6a1c507899b6fb1b9a56069954365f655956bcc648e150ce64c2b0ecbed8/coverage-7.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:3e33a968672be1394eded257ec10d4acbb9af2ae263ba05a99ff901bb863557e", size = 218899, upload-time = "2025-11-10T00:12:46.18Z" }, + { url = "https://files.pythonhosted.org/packages/9a/58/142cd838d960cd740654d094f7b0300d7b81534bb7304437d2439fb685fb/coverage-7.11.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f9c96a29c6d65bd36a91f5634fef800212dff69dacdb44345c4c9783943ab0df", size = 217471, upload-time = "2025-11-10T00:12:48.392Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2c/2f44d39eb33e41ab3aba80571daad32e0f67076afcf27cb443f9e5b5a3ee/coverage-7.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2ec27a7a991d229213c8070d31e3ecf44d005d96a9edc30c78eaeafaa421c001", size = 217742, upload-time = "2025-11-10T00:12:50.182Z" }, + { url = "https://files.pythonhosted.org/packages/32/76/8ebc66c3c699f4de3174a43424c34c086323cd93c4930ab0f835731c443a/coverage-7.11.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:72c8b494bd20ae1c58528b97c4a67d5cfeafcb3845c73542875ecd43924296de", size = 259120, upload-time = "2025-11-10T00:12:52.451Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/78a3302b9595f331b86e4f12dfbd9252c8e93d97b8631500888f9a3a2af7/coverage-7.11.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:60ca149a446da255d56c2a7a813b51a80d9497a62250532598d249b3cdb1a926", size = 261229, upload-time = "2025-11-10T00:12:54.667Z" }, + { url = "https://files.pythonhosted.org/packages/07/59/1a9c0844dadef2a6efac07316d9781e6c5a3f3ea7e5e701411e99d619bfd/coverage-7.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb5069074db19a534de3859c43eec78e962d6d119f637c41c8e028c5ab3f59dd", size = 263642, upload-time = "2025-11-10T00:12:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/37/86/66c15d190a8e82eee777793cabde730640f555db3c020a179625a2ad5320/coverage-7.11.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac5d5329c9c942bbe6295f4251b135d860ed9f86acd912d418dce186de7c19ac", size = 258193, upload-time = "2025-11-10T00:12:58.687Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c7/4a4aeb25cb6f83c3ec4763e5f7cc78da1c6d4ef9e22128562204b7f39390/coverage-7.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e22539b676fafba17f0a90ac725f029a309eb6e483f364c86dcadee060429d46", size = 261107, upload-time = "2025-11-10T00:13:00.502Z" }, + { url = "https://files.pythonhosted.org/packages/ed/91/b986b5035f23cf0272446298967ecdd2c3c0105ee31f66f7e6b6948fd7f8/coverage-7.11.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2376e8a9c889016f25472c452389e98bc6e54a19570b107e27cde9d47f387b64", size = 258717, upload-time = "2025-11-10T00:13:02.747Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/6c084997f5a04d050c513545d3344bfa17bd3b67f143f388b5757d762b0b/coverage-7.11.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4234914b8c67238a3c4af2bba648dc716aa029ca44d01f3d51536d44ac16854f", size = 257541, upload-time = "2025-11-10T00:13:04.689Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c5/38e642917e406930cb67941210a366ccffa767365c8f8d9ec0f465a8b218/coverage-7.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0b4101e2b3c6c352ff1f70b3a6fcc7c17c1ab1a91ccb7a33013cb0782af9820", size = 259872, upload-time = "2025-11-10T00:13:06.559Z" }, + { url = "https://files.pythonhosted.org/packages/b7/67/5e812979d20c167f81dbf9374048e0193ebe64c59a3d93d7d947b07865fa/coverage-7.11.3-cp314-cp314t-win32.whl", hash = "sha256:305716afb19133762e8cf62745c46c4853ad6f9eeba54a593e373289e24ea237", size = 220289, upload-time = "2025-11-10T00:13:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/24/3a/b72573802672b680703e0df071faadfab7dcd4d659aaaffc4626bc8bbde8/coverage-7.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9245bd392572b9f799261c4c9e7216bafc9405537d0f4ce3ad93afe081a12dc9", size = 221398, upload-time = "2025-11-10T00:13:10.734Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4e/649628f28d38bad81e4e8eb3f78759d20ac173e3c456ac629123815feb40/coverage-7.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:9a1d577c20b4334e5e814c3d5fe07fa4a8c3ae42a601945e8d7940bab811d0bd", size = 219435, upload-time = "2025-11-10T00:13:12.712Z" }, + { url = "https://files.pythonhosted.org/packages/19/8f/92bdd27b067204b99f396a1414d6342122f3e2663459baf787108a6b8b84/coverage-7.11.3-py3-none-any.whl", hash = "sha256:351511ae28e2509c8d8cae5311577ea7dd511ab8e746ffc8814a0896c3d33fbe", size = 208478, upload-time = "2025-11-10T00:13:14.908Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + +[[package]] +name = "eval-type-backport" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/ea/8b0ac4469d4c347c6a385ff09dc3c048c2d021696664e26c7ee6791631b5/eval_type_backport-0.2.2.tar.gz", hash = "sha256:f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1", size = 9079, upload-time = "2024-12-21T20:09:46.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/31/55cd413eaccd39125368be33c46de24a1f639f2e12349b0361b4678f3915/eval_type_backport-0.2.2-py3-none-any.whl", hash = "sha256:cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a", size = 5830, upload-time = "2024-12-21T20:09:44.175Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fastapi" +version = "0.115.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/64/ec0788201b5554e2a87c49af26b77a4d132f807a0fa9675257ac92c6aa0e/fastapi-0.115.13.tar.gz", hash = "sha256:55d1d25c2e1e0a0a50aceb1c8705cd932def273c102bff0b1c1da88b3c6eb307", size = 295680, upload-time = "2025-06-17T11:49:45.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/4a/e17764385382062b0edbb35a26b7cf76d71e27e456546277a42ba6545c6e/fastapi-0.115.13-py3-none-any.whl", hash = "sha256:0a0cab59afa7bab22f5eb347f8c9864b681558c278395e94035a741fc10cd865", size = 95315, upload-time = "2025-06-17T11:49:44.106Z" }, +] + +[[package]] +name = "fastavro" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/8b/fa2d3287fd2267be6261d0177c6809a7fa12c5600ddb33490c8dc29e77b2/fastavro-1.12.1.tar.gz", hash = "sha256:2f285be49e45bc047ab2f6bed040bb349da85db3f3c87880e4b92595ea093b2b", size = 1025661, upload-time = "2025-10-10T15:40:55.41Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/a0/077fd7cbfc143152cb96780cb592ed6cb6696667d8bc1b977745eb2255a8/fastavro-1.12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:00650ca533907361edda22e6ffe8cf87ab2091c5d8aee5c8000b0f2dcdda7ed3", size = 1000335, upload-time = "2025-10-10T15:40:59.834Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ae/a115e027f3a75df237609701b03ecba0b7f0aa3d77fe0161df533fde1eb7/fastavro-1.12.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac76d6d95f909c72ee70d314b460b7e711d928845771531d823eb96a10952d26", size = 3221067, upload-time = "2025-10-10T15:41:04.399Z" }, + { url = "https://files.pythonhosted.org/packages/94/4e/c4991c3eec0175af9a8a0c161b88089cb7bf7fe353b3e3be1bc4cf9036b2/fastavro-1.12.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55eef18c41d4476bd32a82ed5dd86aabc3f614e1b66bdb09ffa291612e1670", size = 3228979, upload-time = "2025-10-10T15:41:06.738Z" }, + { url = "https://files.pythonhosted.org/packages/21/0c/f2afb8eaea38799ccb1ed07d68bf2659f2e313f1902bbd36774cf6a1bef9/fastavro-1.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81563e1f93570e6565487cdb01ba241a36a00e58cff9c5a0614af819d1155d8f", size = 3160740, upload-time = "2025-10-10T15:41:08.731Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1a/f4d367924b40b86857862c1fa65f2afba94ddadf298b611e610a676a29e5/fastavro-1.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bec207360f76f0b3de540758a297193c5390e8e081c43c3317f610b1414d8c8f", size = 3235787, upload-time = "2025-10-10T15:41:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/8db9331896e3dfe4f71b2b3c23f2e97fbbfd90129777467ca9f8bafccb74/fastavro-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:c0390bfe4a9f8056a75ac6785fbbff8f5e317f5356481d2e29ec980877d2314b", size = 449350, upload-time = "2025-10-10T15:41:12.104Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e9/31c64b47cefc0951099e7c0c8c8ea1c931edd1350f34d55c27cbfbb08df1/fastavro-1.12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b632b713bc5d03928a87d811fa4a11d5f25cd43e79c161e291c7d3f7aa740fd", size = 1016585, upload-time = "2025-10-10T15:41:13.717Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/111560775b548f5d8d828c1b5285ff90e2d2745643fb80ecbf115344eea4/fastavro-1.12.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa7ab3769beadcebb60f0539054c7755f63bd9cf7666e2c15e615ab605f89a8", size = 3404629, upload-time = "2025-10-10T15:41:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/b0/07/6bb93cb963932146c2b6c5c765903a0a547ad9f0f8b769a4a9aad8c06369/fastavro-1.12.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123fb221df3164abd93f2d042c82f538a1d5a43ce41375f12c91ce1355a9141e", size = 3428594, upload-time = "2025-10-10T15:41:17.779Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8115ec36b584197ea737ec79e3499e1f1b640b288d6c6ee295edd13b80f6/fastavro-1.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:632a4e3ff223f834ddb746baae0cc7cee1068eb12c32e4d982c2fee8a5b483d0", size = 3344145, upload-time = "2025-10-10T15:41:19.89Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9e/a7cebb3af967e62539539897c10138fa0821668ec92525d1be88a9cd3ee6/fastavro-1.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e6caf4e7a8717d932a3b1ff31595ad169289bbe1128a216be070d3a8391671", size = 3431942, upload-time = "2025-10-10T15:41:22.076Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d1/7774ddfb8781c5224294c01a593ebce2ad3289b948061c9701bd1903264d/fastavro-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:b91a0fe5a173679a6c02d53ca22dcaad0a2c726b74507e0c1c2e71a7c3f79ef9", size = 450542, upload-time = "2025-10-10T15:41:23.333Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f0/10bd1a3d08667fa0739e2b451fe90e06df575ec8b8ba5d3135c70555c9bd/fastavro-1.12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:509818cb24b98a804fc80be9c5fed90f660310ae3d59382fc811bfa187122167", size = 1009057, upload-time = "2025-10-10T15:41:24.556Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/0d985bc99e1fa9e74c636658000ba38a5cd7f5ab2708e9c62eaf736ecf1a/fastavro-1.12.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089e155c0c76e0d418d7e79144ce000524dd345eab3bc1e9c5ae69d500f71b14", size = 3391866, upload-time = "2025-10-10T15:41:26.882Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9e/b4951dc84ebc34aac69afcbfbb22ea4a91080422ec2bfd2c06076ff1d419/fastavro-1.12.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44cbff7518901c91a82aab476fcab13d102e4999499df219d481b9e15f61af34", size = 3458005, upload-time = "2025-10-10T15:41:29.017Z" }, + { url = "https://files.pythonhosted.org/packages/af/f8/5a8df450a9f55ca8441f22ea0351d8c77809fc121498b6970daaaf667a21/fastavro-1.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a275e48df0b1701bb764b18a8a21900b24cf882263cb03d35ecdba636bbc830b", size = 3295258, upload-time = "2025-10-10T15:41:31.564Z" }, + { url = "https://files.pythonhosted.org/packages/99/b2/40f25299111d737e58b85696e91138a66c25b7334f5357e7ac2b0e8966f8/fastavro-1.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2de72d786eb38be6b16d556b27232b1bf1b2797ea09599507938cdb7a9fe3e7c", size = 3430328, upload-time = "2025-10-10T15:41:33.689Z" }, + { url = "https://files.pythonhosted.org/packages/e0/07/85157a7c57c5f8b95507d7829b5946561e5ee656ff80e9dd9a757f53ddaf/fastavro-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:9090f0dee63fe022ee9cc5147483366cc4171c821644c22da020d6b48f576b4f", size = 444140, upload-time = "2025-10-10T15:41:34.902Z" }, + { url = "https://files.pythonhosted.org/packages/bb/57/26d5efef9182392d5ac9f253953c856ccb66e4c549fd3176a1e94efb05c9/fastavro-1.12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78df838351e4dff9edd10a1c41d1324131ffecbadefb9c297d612ef5363c049a", size = 1000599, upload-time = "2025-10-10T15:41:36.554Z" }, + { url = "https://files.pythonhosted.org/packages/33/cb/8ab55b21d018178eb126007a56bde14fd01c0afc11d20b5f2624fe01e698/fastavro-1.12.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:780476c23175d2ae457c52f45b9ffa9d504593499a36cd3c1929662bf5b7b14b", size = 3335933, upload-time = "2025-10-10T15:41:39.07Z" }, + { url = "https://files.pythonhosted.org/packages/fe/03/9c94ec9bf873eb1ffb0aa694f4e71940154e6e9728ddfdc46046d7e8ced4/fastavro-1.12.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0714b285160fcd515eb0455540f40dd6dac93bdeacdb03f24e8eac3d8aa51f8d", size = 3402066, upload-time = "2025-10-10T15:41:41.608Z" }, + { url = "https://files.pythonhosted.org/packages/75/c8/cb472347c5a584ccb8777a649ebb28278fccea39d005fc7df19996f41df8/fastavro-1.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a8bc2dcec5843d499f2489bfe0747999108f78c5b29295d877379f1972a3d41a", size = 3240038, upload-time = "2025-10-10T15:41:43.743Z" }, + { url = "https://files.pythonhosted.org/packages/e1/77/569ce9474c40304b3a09e109494e020462b83e405545b78069ddba5f614e/fastavro-1.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3b1921ac35f3d89090a5816b626cf46e67dbecf3f054131f84d56b4e70496f45", size = 3369398, upload-time = "2025-10-10T15:41:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1f/9589e35e9ea68035385db7bdbf500d36b8891db474063fb1ccc8215ee37c/fastavro-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:5aa777b8ee595b50aa084104cd70670bf25a7bbb9fd8bb5d07524b0785ee1699", size = 444220, upload-time = "2025-10-10T15:41:47.39Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d2/78435fe737df94bd8db2234b2100f5453737cffd29adee2504a2b013de84/fastavro-1.12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c3d67c47f177e486640404a56f2f50b165fe892cc343ac3a34673b80cc7f1dd6", size = 1086611, upload-time = "2025-10-10T15:41:48.818Z" }, + { url = "https://files.pythonhosted.org/packages/b6/be/428f99b10157230ddac77ec8cc167005b29e2bd5cbe228345192bb645f30/fastavro-1.12.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5217f773492bac43dae15ff2931432bce2d7a80be7039685a78d3fab7df910bd", size = 3541001, upload-time = "2025-10-10T15:41:50.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/08/a2eea4f20b85897740efe44887e1ac08f30dfa4bfc3de8962bdcbb21a5a1/fastavro-1.12.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:469fecb25cba07f2e1bfa4c8d008477cd6b5b34a59d48715e1b1a73f6160097d", size = 3432217, upload-time = "2025-10-10T15:41:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/87/bb/b4c620b9eb6e9838c7f7e4b7be0762834443adf9daeb252a214e9ad3178c/fastavro-1.12.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d71c8aa841ef65cfab709a22bb887955f42934bced3ddb571e98fdbdade4c609", size = 3366742, upload-time = "2025-10-10T15:41:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d1/e69534ccdd5368350646fea7d93be39e5f77c614cca825c990bd9ca58f67/fastavro-1.12.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b81fc04e85dfccf7c028e0580c606e33aa8472370b767ef058aae2c674a90746", size = 3383743, upload-time = "2025-10-10T15:41:57.68Z" }, + { url = "https://files.pythonhosted.org/packages/58/54/b7b4a0c3fb5fcba38128542da1b26c4e6d69933c923f493548bdfd63ab6a/fastavro-1.12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9445da127751ba65975d8e4bdabf36bfcfdad70fc35b2d988e3950cce0ec0e7c", size = 1001377, upload-time = "2025-10-10T15:41:59.241Z" }, + { url = "https://files.pythonhosted.org/packages/1e/4f/0e589089c7df0d8f57d7e5293fdc34efec9a3b758a0d4d0c99a7937e2492/fastavro-1.12.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed924233272719b5d5a6a0b4d80ef3345fc7e84fc7a382b6232192a9112d38a6", size = 3320401, upload-time = "2025-10-10T15:42:01.682Z" }, + { url = "https://files.pythonhosted.org/packages/f9/19/260110d56194ae29d7e423a336fccea8bcd103196d00f0b364b732bdb84e/fastavro-1.12.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3616e2f0e1c9265e92954fa099db79c6e7817356d3ff34f4bcc92699ae99697c", size = 3350894, upload-time = "2025-10-10T15:42:04.073Z" }, + { url = "https://files.pythonhosted.org/packages/d0/96/58b0411e8be9694d5972bee3167d6c1fd1fdfdf7ce253c1a19a327208f4f/fastavro-1.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cb0337b42fd3c047fcf0e9b7597bd6ad25868de719f29da81eabb6343f08d399", size = 3229644, upload-time = "2025-10-10T15:42:06.221Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/38660660eac82c30471d9101f45b3acfdcbadfe42d8f7cdb129459a45050/fastavro-1.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:64961ab15b74b7c168717bbece5660e0f3d457837c3cc9d9145181d011199fa7", size = 3329704, upload-time = "2025-10-10T15:42:08.384Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a9/1672910f458ecb30b596c9e59e41b7c00309b602a0494341451e92e62747/fastavro-1.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:792356d320f6e757e89f7ac9c22f481e546c886454a6709247f43c0dd7058004", size = 452911, upload-time = "2025-10-10T15:42:09.795Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/2e15d0938ded1891b33eff252e8500605508b799c2e57188a933f0bd744c/fastavro-1.12.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:120aaf82ac19d60a1016afe410935fe94728752d9c2d684e267e5b7f0e70f6d9", size = 3541999, upload-time = "2025-10-10T15:42:11.794Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1c/6dfd082a205be4510543221b734b1191299e6a1810c452b6bc76dfa6968e/fastavro-1.12.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6a3462934b20a74f9ece1daa49c2e4e749bd9a35fa2657b53bf62898fba80f5", size = 3433972, upload-time = "2025-10-10T15:42:14.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/9de694625a1a4b727b1ad0958d220cab25a9b6cf7f16a5c7faa9ea7b2261/fastavro-1.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1f81011d54dd47b12437b51dd93a70a9aa17b61307abf26542fc3c13efbc6c51", size = 3368752, upload-time = "2025-10-10T15:42:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/fa/93/b44f67589e4d439913dab6720f7e3507b0fa8b8e56d06f6fc875ced26afb/fastavro-1.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43ded16b3f4a9f1a42f5970c2aa618acb23ea59c4fcaa06680bdf470b255e5a8", size = 3386636, upload-time = "2025-10-10T15:42:18.974Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, +] + +[[package]] +name = "frozendict" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/59/19eb300ba28e7547538bdf603f1c6c34793240a90e1a7b61b65d8517e35e/frozendict-2.4.6.tar.gz", hash = "sha256:df7cd16470fbd26fc4969a208efadc46319334eb97def1ddf48919b351192b8e", size = 316416, upload-time = "2024-10-13T12:15:32.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/7f/e80cdbe0db930b2ba9d46ca35a41b0150156da16dfb79edcc05642690c3b/frozendict-2.4.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3a05c0a50cab96b4bb0ea25aa752efbfceed5ccb24c007612bc63e51299336f", size = 37927, upload-time = "2024-10-13T12:14:17.927Z" }, + { url = "https://files.pythonhosted.org/packages/29/98/27e145ff7e8e63caa95fb8ee4fc56c68acb208bef01a89c3678a66f9a34d/frozendict-2.4.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f5b94d5b07c00986f9e37a38dd83c13f5fe3bf3f1ccc8e88edea8fe15d6cd88c", size = 37945, upload-time = "2024-10-13T12:14:19.976Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f1/a10be024a9d53441c997b3661ea80ecba6e3130adc53812a4b95b607cdd1/frozendict-2.4.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c789fd70879ccb6289a603cdebdc4953e7e5dea047d30c1b180529b28257b5", size = 117656, upload-time = "2024-10-13T12:14:22.038Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/34c760975e6f1cb4db59a990d58dcf22287e10241c851804670c74c6a27a/frozendict-2.4.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da6a10164c8a50b34b9ab508a9420df38f4edf286b9ca7b7df8a91767baecb34", size = 117444, upload-time = "2024-10-13T12:14:24.251Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/64bddd1ffa9617f50e7e63656b2a7ad7f0a46c86b5f4a3d2c714d0006277/frozendict-2.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9a8a43036754a941601635ea9c788ebd7a7efbed2becba01b54a887b41b175b9", size = 116801, upload-time = "2024-10-13T12:14:26.518Z" }, + { url = "https://files.pythonhosted.org/packages/45/ae/af06a8bde1947277aad895c2f26c3b8b8b6ee9c0c2ad988fb58a9d1dde3f/frozendict-2.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9905dcf7aa659e6a11b8051114c9fa76dfde3a6e50e6dc129d5aece75b449a2", size = 117329, upload-time = "2024-10-13T12:14:28.485Z" }, + { url = "https://files.pythonhosted.org/packages/d2/df/be3fa0457ff661301228f4c59c630699568c8ed9b5480f113b3eea7d0cb3/frozendict-2.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:323f1b674a2cc18f86ab81698e22aba8145d7a755e0ac2cccf142ee2db58620d", size = 37522, upload-time = "2024-10-13T12:14:30.418Z" }, + { url = "https://files.pythonhosted.org/packages/4a/6f/c22e0266b4c85f58b4613fec024e040e93753880527bf92b0c1bc228c27c/frozendict-2.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:eabd21d8e5db0c58b60d26b4bb9839cac13132e88277e1376970172a85ee04b3", size = 34056, upload-time = "2024-10-13T12:14:31.757Z" }, + { url = "https://files.pythonhosted.org/packages/04/13/d9839089b900fa7b479cce495d62110cddc4bd5630a04d8469916c0e79c5/frozendict-2.4.6-py311-none-any.whl", hash = "sha256:d065db6a44db2e2375c23eac816f1a022feb2fa98cbb50df44a9e83700accbea", size = 16148, upload-time = "2024-10-13T12:15:26.839Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d0/d482c39cee2ab2978a892558cf130681d4574ea208e162da8958b31e9250/frozendict-2.4.6-py312-none-any.whl", hash = "sha256:49344abe90fb75f0f9fdefe6d4ef6d4894e640fadab71f11009d52ad97f370b9", size = 16146, upload-time = "2024-10-13T12:15:28.16Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8e/b6bf6a0de482d7d7d7a2aaac8fdc4a4d0bb24a809f5ddd422aa7060eb3d2/frozendict-2.4.6-py313-none-any.whl", hash = "sha256:7134a2bb95d4a16556bb5f2b9736dceb6ea848fa5b6f3f6c2d6dba93b44b4757", size = 16146, upload-time = "2024-10-13T12:15:29.495Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" }, +] + +[[package]] +name = "genai-prices" +version = "0.0.38" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eval-type-backport", marker = "python_full_version < '3.11'" }, + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/31/eacbfbdb8086869ecb4a1690936900e5e0f0ccd5fd9dc3f5278b27c792c0/genai_prices-0.0.38.tar.gz", hash = "sha256:27295d0db523cc5e98aea656a297b78c768cceeab03608a0dd5b029e095229ca", size = 47036, upload-time = "2025-11-10T11:46:40.066Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/ae/93b67a2404482e75791278b60fe84a222da4116af080a49db5ec336e5495/genai_prices-0.0.38-py3-none-any.whl", hash = "sha256:c1ddf9040877116e23c4e1dfb390f8e9683b533c758e5d756494e1a26159ea4f", size = 49688, upload-time = "2025-11-10T11:46:36.988Z" }, +] + +[[package]] +name = "gimie" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "calamus" }, + { name = "gitpython" }, + { name = "numpy" }, + { name = "pydantic" }, + { name = "pydriller" }, + { name = "python-dateutil" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "spdx-license-list" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/82/64e6dccee6a8c3772d382a7fc3cf04fcb76358eaafd86d163e56fcd26f62/gimie-0.7.2.tar.gz", hash = "sha256:a0f697e0643540785e62261c2afa2fa5c4ed3a8eef6583ccded9f691d122dddd", size = 96775, upload-time = "2024-12-18T09:05:46.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/96/f3cb8d114d1d1f3c97e762daed5f2851c8048e221da80949986c7047ba86/gimie-0.7.2-py3-none-any.whl", hash = "sha256:7da9185adebe27b7deee88a6617ae59b2f3b3e7ccf5058900be28a0047e4efe5", size = 107177, upload-time = "2024-12-18T09:05:42.993Z" }, +] + +[[package]] +name = "git-metadata-extractor" +version = "2.0.0" +source = { editable = "." } +dependencies = [ + { name = "aiohttp" }, + { name = "beautifulsoup4" }, + { name = "fastapi" }, + { name = "gimie" }, + { name = "google-genai" }, + { name = "httpx" }, + { name = "markdownify" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "pydantic-ai" }, + { name = "pyld" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "rdflib" }, + { name = "rdflib-jsonld" }, + { name = "repo-to-text" }, + { name = "requests" }, + { name = "selenium" }, + { name = "tiktoken" }, + { name = "uvicorn", extra = ["standard"] }, + { name = "uvicorn-worker" }, +] + +[package.optional-dependencies] +dev = [ + { name = "black" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", specifier = "==3.12.15" }, + { name = "beautifulsoup4", specifier = "==4.13.4" }, + { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, + { name = "fastapi", specifier = "==0.115.13" }, + { name = "gimie", specifier = "==0.7.2" }, + { name = "google-genai", specifier = ">=1.31.0" }, + { name = "httpx" }, + { name = "markdownify", specifier = "==1.2.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0.0" }, + { name = "openai", specifier = "==2.1.0" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.0.0" }, + { name = "pydantic", specifier = "==2.11.7" }, + { name = "pydantic-ai", specifier = ">=1.0.15" }, + { name = "pyld", specifier = "==2.0.4" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" }, + { name = "python-dotenv", specifier = "==0.21.1" }, + { name = "pyyaml", specifier = "==6.0.2" }, + { name = "rdflib", specifier = "==6.2.0" }, + { name = "rdflib-jsonld", specifier = "==0.6.2" }, + { name = "repo-to-text", specifier = ">=0.7.0" }, + { name = "requests", specifier = "==2.32.4" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, + { name = "selenium", specifier = "==4.34.2" }, + { name = "tiktoken", specifier = "==0.9.0" }, + { name = "uvicorn", extras = ["standard"], specifier = "==0.34.3" }, + { name = "uvicorn-worker", specifier = "==0.3.0" }, +] +provides-extras = ["dev"] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, +] + +[[package]] +name = "google-auth" +version = "2.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/ef/66d14cf0e01b08d2d51ffc3c20410c4e134a1548fc246a6081eae585a4fe/google_auth-2.43.0.tar.gz", hash = "sha256:88228eee5fc21b62a1b5fe773ca15e67778cb07dc8363adcb4a8827b52d81483", size = 296359, upload-time = "2025-11-06T00:13:36.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/d1/385110a9ae86d91cc14c5282c61fe9f4dc41c0b9f7d423c6ad77038c4448/google_auth-2.43.0-py2.py3-none-any.whl", hash = "sha256:af628ba6fa493f75c7e9dbe9373d148ca9f4399b5ea29976519e0a3848eddd16", size = 223114, upload-time = "2025-11-06T00:13:35.209Z" }, +] + +[[package]] +name = "google-genai" +version = "1.49.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "google-auth" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/49/1a724ee3c3748fa50721d53a52d9fee88c67d0c43bb16eb2b10ee89ab239/google_genai-1.49.0.tar.gz", hash = "sha256:35eb16023b72e298571ae30e919c810694f258f2ba68fc77a2185c7c8829ad5a", size = 253493, upload-time = "2025-11-05T22:41:03.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/d3/84a152746dc7bdebb8ba0fd7d6157263044acd1d14b2a53e8df4a307b6b7/google_genai-1.49.0-py3-none-any.whl", hash = "sha256:ad49cd5be5b63397069e7aef9a4fe0a84cbdf25fcd93408e795292308db4ef32", size = 256098, upload-time = "2025-11-05T22:41:01.429Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, +] + +[[package]] +name = "griffe" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, +] + +[[package]] +name = "groq" +version = "0.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/51/b85f8100078a4802340e8325af2bfa357e3e8d367f11ee8fd83dc3441523/groq-0.33.0.tar.gz", hash = "sha256:5342158026a1f6bf58653d774696f47ef1d763c401e20f9dbc9598337859523a", size = 142470, upload-time = "2025-10-21T01:38:49.913Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/5ecd95278f6f1793bccd9ffa0b6db0d8eb71acda9be9dd0668b162fc2986/groq-0.33.0-py3-none-any.whl", hash = "sha256:ed8c33e55872dea3c7a087741af0c36c2a1a6699a24a34f6cada53e502d3ad75", size = 135782, upload-time = "2025-10-21T01:38:48.855Z" }, +] + +[[package]] +name = "gunicorn" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, + { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" }, + { url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" }, + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "shellingham" }, + { name = "tqdm" }, + { name = "typer-slim" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/63/eeea214a6b456d8e91ac2ea73ebb83da3af9aa64716dfb6e28dd9b2e6223/huggingface_hub-1.1.2.tar.gz", hash = "sha256:7bdafc432dc12fa1f15211bdfa689a02531d2a47a3cc0d74935f5726cdbcab8e", size = 606173, upload-time = "2025-11-06T10:04:38.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/21/e15d90fd09b56938502a0348d566f1915f9789c5bb6c00c1402dc7259b6e/huggingface_hub-1.1.2-py3-none-any.whl", hash = "sha256:dfcfa84a043466fac60573c3e4af475490a7b0d7375b22e3817706d6659f61f7", size = 514955, upload-time = "2025-11-06T10:04:36.674Z" }, +] + +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "invoke" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/bd/b461d3424a24c80490313fd77feeb666ca4f6a28c7e72713e3d9095719b4/invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707", size = 304762, upload-time = "2025-10-11T00:36:35.172Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/4b/b99e37f88336009971405cbb7630610322ed6fbfa31e1d7ab3fbf3049a2d/invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8", size = 160287, upload-time = "2025-10-11T00:36:33.703Z" }, +] + +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + +[[package]] +name = "jiter" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/91/13cb9505f7be74a933f37da3af22e029f6ba64f5669416cb8b2774bc9682/jiter-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7acbaba9703d5de82a2c98ae6a0f59ab9770ab5af5fa35e43a303aee962cf65", size = 316652, upload-time = "2025-11-09T20:46:41.021Z" }, + { url = "https://files.pythonhosted.org/packages/4e/76/4e9185e5d9bb4e482cf6dec6410d5f78dfeb374cfcecbbe9888d07c52daa/jiter-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:364f1a7294c91281260364222f535bc427f56d4de1d8ffd718162d21fbbd602e", size = 319829, upload-time = "2025-11-09T20:46:43.281Z" }, + { url = "https://files.pythonhosted.org/packages/86/af/727de50995d3a153138139f259baae2379d8cb0522c0c00419957bc478a6/jiter-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ee4d25805d4fb23f0a5167a962ef8e002dbfb29c0989378488e32cf2744b62", size = 350568, upload-time = "2025-11-09T20:46:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c1/d6e9f4b7a3d5ac63bcbdfddeb50b2dcfbdc512c86cffc008584fdc350233/jiter-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:796f466b7942107eb889c08433b6e31b9a7ed31daceaecf8af1be26fb26c0ca8", size = 369052, upload-time = "2025-11-09T20:46:46.818Z" }, + { url = "https://files.pythonhosted.org/packages/eb/be/00824cd530f30ed73fa8a4f9f3890a705519e31ccb9e929f1e22062e7c76/jiter-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35506cb71f47dba416694e67af996bbdefb8e3608f1f78799c2e1f9058b01ceb", size = 481585, upload-time = "2025-11-09T20:46:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/74/b6/2ad7990dff9504d4b5052eef64aa9574bd03d722dc7edced97aad0d47be7/jiter-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726c764a90c9218ec9e4f99a33d6bf5ec169163f2ca0fc21b654e88c2abc0abc", size = 380541, upload-time = "2025-11-09T20:46:49.643Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c7/f3c26ecbc1adbf1db0d6bba99192143d8fe8504729d9594542ecc4445784/jiter-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa47810c5565274810b726b0dc86d18dce5fd17b190ebdc3890851d7b2a0e74", size = 364423, upload-time = "2025-11-09T20:46:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/18/51/eac547bf3a2d7f7e556927278e14c56a0604b8cddae75815d5739f65f81d/jiter-0.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ec0259d3f26c62aed4d73b198c53e316ae11f0f69c8fbe6682c6dcfa0fcce2", size = 389958, upload-time = "2025-11-09T20:46:53.432Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1f/9ca592e67175f2db156cff035e0d817d6004e293ee0c1d73692d38fcb596/jiter-0.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:79307d74ea83465b0152fa23e5e297149506435535282f979f18b9033c0bb025", size = 522084, upload-time = "2025-11-09T20:46:54.848Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/597d9cdc3028f28224f53e1a9d063628e28b7a5601433e3196edda578cdd/jiter-0.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf6e6dd18927121fec86739f1a8906944703941d000f0639f3eb6281cc601dca", size = 513054, upload-time = "2025-11-09T20:46:56.487Z" }, + { url = "https://files.pythonhosted.org/packages/24/6d/1970bce1351bd02e3afcc5f49e4f7ef3dabd7fb688f42be7e8091a5b809a/jiter-0.12.0-cp310-cp310-win32.whl", hash = "sha256:b6ae2aec8217327d872cbfb2c1694489057b9433afce447955763e6ab015b4c4", size = 206368, upload-time = "2025-11-09T20:46:58.638Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6b/eb1eb505b2d86709b59ec06681a2b14a94d0941db091f044b9f0e16badc0/jiter-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7f49ce90a71e44f7e1aa9e7ec415b9686bbc6a5961e57eab511015e6759bc11", size = 204847, upload-time = "2025-11-09T20:47:00.295Z" }, + { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, + { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" }, + { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" }, + { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" }, + { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" }, + { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" }, + { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" }, + { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, + { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, + { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, + { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, + { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, + { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, + { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, + { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, + { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, + { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, + { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, + { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, + { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, + { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" }, + { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" }, + { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, + { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "lazy-object-proxy" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/2b/d5e8915038acbd6c6a9fcb8aaf923dc184222405d3710285a1fec6e262bc/lazy_object_proxy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61d5e3310a4aa5792c2b599a7a78ccf8687292c8eb09cf187cca8f09cf6a7519", size = 26658, upload-time = "2025-08-22T13:42:23.373Z" }, + { url = "https://files.pythonhosted.org/packages/da/8f/91fc00eeea46ee88b9df67f7c5388e60993341d2a406243d620b2fdfde57/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ca33565f698ac1aece152a10f432415d1a2aa9a42dfe23e5ba2bc255ab91f6", size = 68412, upload-time = "2025-08-22T13:42:24.727Z" }, + { url = "https://files.pythonhosted.org/packages/07/d2/b7189a0e095caedfea4d42e6b6949d2685c354263bdf18e19b21ca9b3cd6/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01c7819a410f7c255b20799b65d36b414379a30c6f1684c7bd7eb6777338c1b", size = 67559, upload-time = "2025-08-22T13:42:25.875Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b013840cc43971582ff1ceaf784d35d3a579650eb6cc348e5e6ed7e34d28/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:029d2b355076710505c9545aef5ab3f750d89779310e26ddf2b7b23f6ea03cd8", size = 66651, upload-time = "2025-08-22T13:42:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/7e/6f/b7368d301c15612fcc4cd00412b5d6ba55548bde09bdae71930e1a81f2ab/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc6e3614eca88b1c8a625fc0a47d0d745e7c3255b21dac0e30b3037c5e3deeb8", size = 66901, upload-time = "2025-08-22T13:42:28.585Z" }, + { url = "https://files.pythonhosted.org/packages/61/1b/c6b1865445576b2fc5fa0fbcfce1c05fee77d8979fd1aa653dd0f179aefc/lazy_object_proxy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:be5fe974e39ceb0d6c9db0663c0464669cf866b2851c73971409b9566e880eab", size = 26536, upload-time = "2025-08-22T13:42:29.636Z" }, + { url = "https://files.pythonhosted.org/packages/01/b3/4684b1e128a87821e485f5a901b179790e6b5bc02f89b7ee19c23be36ef3/lazy_object_proxy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf69cd1a6c7fe2dbcc3edaa017cf010f4192e53796538cc7d5e1fedbfa4bcff", size = 26656, upload-time = "2025-08-22T13:42:30.605Z" }, + { url = "https://files.pythonhosted.org/packages/3a/03/1bdc21d9a6df9ff72d70b2ff17d8609321bea4b0d3cffd2cea92fb2ef738/lazy_object_proxy-1.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efff4375a8c52f55a145dc8487a2108c2140f0bec4151ab4e1843e52eb9987ad", size = 68832, upload-time = "2025-08-22T13:42:31.675Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4b/5788e5e8bd01d19af71e50077ab020bc5cce67e935066cd65e1215a09ff9/lazy_object_proxy-1.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1192e8c2f1031a6ff453ee40213afa01ba765b3dc861302cd91dbdb2e2660b00", size = 69148, upload-time = "2025-08-22T13:42:32.876Z" }, + { url = "https://files.pythonhosted.org/packages/79/0e/090bf070f7a0de44c61659cb7f74c2fe02309a77ca8c4b43adfe0b695f66/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3605b632e82a1cbc32a1e5034278a64db555b3496e0795723ee697006b980508", size = 67800, upload-time = "2025-08-22T13:42:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d2/b320325adbb2d119156f7c506a5fbfa37fcab15c26d13cf789a90a6de04e/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a61095f5d9d1a743e1e20ec6d6db6c2ca511961777257ebd9b288951b23b44fa", size = 68085, upload-time = "2025-08-22T13:42:35.197Z" }, + { url = "https://files.pythonhosted.org/packages/6a/48/4b718c937004bf71cd82af3713874656bcb8d0cc78600bf33bb9619adc6c/lazy_object_proxy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:997b1d6e10ecc6fb6fe0f2c959791ae59599f41da61d652f6c903d1ee58b7370", size = 26535, upload-time = "2025-08-22T13:42:36.521Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" }, + { url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/e581ffed864cd33c1b445b5763d617448ebb880f48675fc9de0471a95cbc/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308", size = 69329, upload-time = "2025-08-22T13:42:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/15f8f5a0b0b2e668e756a152257d26370132c97f2f1943329b08f057eff0/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23", size = 70690, upload-time = "2025-08-22T13:42:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/5d/aa/f02be9bbfb270e13ee608c2b28b8771f20a5f64356c6d9317b20043c6129/lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073", size = 26563, upload-time = "2025-08-22T13:42:43.685Z" }, + { url = "https://files.pythonhosted.org/packages/f4/26/b74c791008841f8ad896c7f293415136c66cc27e7c7577de4ee68040c110/lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e", size = 26745, upload-time = "2025-08-22T13:42:44.982Z" }, + { url = "https://files.pythonhosted.org/packages/9b/52/641870d309e5d1fb1ea7d462a818ca727e43bfa431d8c34b173eb090348c/lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e", size = 71537, upload-time = "2025-08-22T13:42:46.141Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/919118e99d51c5e76e8bf5a27df406884921c0acf2c7b8a3b38d847ab3e9/lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655", size = 71141, upload-time = "2025-08-22T13:42:47.375Z" }, + { url = "https://files.pythonhosted.org/packages/e5/47/1d20e626567b41de085cf4d4fb3661a56c159feaa73c825917b3b4d4f806/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff", size = 69449, upload-time = "2025-08-22T13:42:48.49Z" }, + { url = "https://files.pythonhosted.org/packages/58/8d/25c20ff1a1a8426d9af2d0b6f29f6388005fc8cd10d6ee71f48bff86fdd0/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be", size = 70744, upload-time = "2025-08-22T13:42:49.608Z" }, + { url = "https://files.pythonhosted.org/packages/c0/67/8ec9abe15c4f8a4bcc6e65160a2c667240d025cbb6591b879bea55625263/lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1", size = 26568, upload-time = "2025-08-22T13:42:57.719Z" }, + { url = "https://files.pythonhosted.org/packages/23/12/cd2235463f3469fd6c62d41d92b7f120e8134f76e52421413a0ad16d493e/lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65", size = 27391, upload-time = "2025-08-22T13:42:50.62Z" }, + { url = "https://files.pythonhosted.org/packages/60/9e/f1c53e39bbebad2e8609c67d0830cc275f694d0ea23d78e8f6db526c12d3/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9", size = 80552, upload-time = "2025-08-22T13:42:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/6c513693448dcb317d9d8c91d91f47addc09553613379e504435b4cc8b3e/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66", size = 82857, upload-time = "2025-08-22T13:42:53.225Z" }, + { url = "https://files.pythonhosted.org/packages/12/1c/d9c4aaa4c75da11eb7c22c43d7c90a53b4fca0e27784a5ab207768debea7/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847", size = 80833, upload-time = "2025-08-22T13:42:54.391Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ae/29117275aac7d7d78ae4f5a4787f36ff33262499d486ac0bf3e0b97889f6/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac", size = 79516, upload-time = "2025-08-22T13:42:55.812Z" }, + { url = "https://files.pythonhosted.org/packages/19/40/b4e48b2c38c69392ae702ae7afa7b6551e0ca5d38263198b7c79de8b3bdf/lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f", size = 27656, upload-time = "2025-08-22T13:42:56.793Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3a/277857b51ae419a1574557c0b12e0d06bf327b758ba94cafc664cb1e2f66/lazy_object_proxy-1.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3", size = 26582, upload-time = "2025-08-22T13:49:49.366Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b6/c5e0fa43535bb9c87880e0ba037cdb1c50e01850b0831e80eb4f4762f270/lazy_object_proxy-1.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a", size = 71059, upload-time = "2025-08-22T13:49:50.488Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/7dcad19c685963c652624702f1a968ff10220b16bfcc442257038216bf55/lazy_object_proxy-1.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a", size = 71034, upload-time = "2025-08-22T13:49:54.224Z" }, + { url = "https://files.pythonhosted.org/packages/12/ac/34cbfb433a10e28c7fd830f91c5a348462ba748413cbb950c7f259e67aa7/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95", size = 69529, upload-time = "2025-08-22T13:49:55.29Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6a/11ad7e349307c3ca4c0175db7a77d60ce42a41c60bcb11800aabd6a8acb8/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5", size = 70391, upload-time = "2025-08-22T13:49:56.35Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" }, + { url = "https://files.pythonhosted.org/packages/41/a0/b91504515c1f9a299fc157967ffbd2f0321bce0516a3d5b89f6f4cad0355/lazy_object_proxy-1.12.0-pp39.pp310.pp311.graalpy311-none-any.whl", hash = "sha256:c3b2e0af1f7f77c4263759c4824316ce458fabe0fceadcd24ef8ca08b2d1e402", size = 15072, upload-time = "2025-08-22T13:50:05.498Z" }, +] + +[[package]] +name = "lizard" +version = "1.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathspec" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/fc/3da7cf04de5de4a1416b8a4084679e95932684bae7d7ba1dcb0e68bbdde7/lizard-1.19.0.tar.gz", hash = "sha256:3e26336ac876bdd2491dbb4afa0d20fe615af11cb59784b8b0ca39b21559ed5c", size = 86131, upload-time = "2025-11-03T08:11:30.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/e7/ddeecbfd0b28d9f01b4ea9eef0055bae30a0181b16ae3eaaae44f4e19e04/lizard-1.19.0-py2.py3-none-any.whl", hash = "sha256:47ce752610c567b9ce427ac2cb4662a0a7631f0791e5ebafdc3572874ea8004f", size = 94142, upload-time = "2025-11-03T08:11:28.137Z" }, +] + +[[package]] +name = "logfire" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "executing" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-sdk" }, + { name = "protobuf" }, + { name = "rich" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/89/d26951b6b21790641720c12cfd6dca0cf7ead0f5ddd7de4299837b90b8b1/logfire-4.14.2.tar.gz", hash = "sha256:8dcedbd59c3d06a8794a93bbf09add788de3b74c45afa821750992f0c822c628", size = 548291, upload-time = "2025-10-24T20:14:39.115Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/92/4fba7b8f4f56f721ad279cb0c08164bffa14e93cfd184d1a4cc7151c52a2/logfire-4.14.2-py3-none-any.whl", hash = "sha256:caa8111b20f263f4ebb0ae380a62f2a214aeb07d5e2f03c9300fa096d0a8e692", size = 228364, upload-time = "2025-10-24T20:14:34.495Z" }, +] + +[package.optional-dependencies] +httpx = [ + { name = "opentelemetry-instrumentation-httpx" }, +] + +[[package]] +name = "logfire-api" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/25/6072086af3b3ac5c2c2f2a6cf89488a1b228ffc6ee0fb357ed1e227efd13/logfire_api-4.14.2.tar.gz", hash = "sha256:bbdeccd931069b76ab811261b41bc52d8b78d1c045fc4b4237dbc085e0fb9bcd", size = 57604, upload-time = "2025-10-24T20:14:40.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/c7/b06a83df678fca882c24fb498e628e0406bdb95ffdfa7ae43ecc0a714d52/logfire_api-4.14.2-py3-none-any.whl", hash = "sha256:aa4af2ecb007c3e0095e25ba4526fd8c0e2c0be2ceceac71ca651c4ad86dc713", size = 95021, upload-time = "2025-10-24T20:14:36.161Z" }, +] + +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/8a/f8192a08237ef2fb1b19733f709db88a4c43bc8ab8357f01cb41a27e7f6a/lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388", size = 8590589, upload-time = "2025-09-22T04:00:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/27bcd07ae17ff5e5536e8d88f4c7d581b48963817a13de11f3ac3329bfa2/lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153", size = 4629671, upload-time = "2025-09-22T04:00:15.411Z" }, + { url = "https://files.pythonhosted.org/packages/02/5a/a7d53b3291c324e0b6e48f3c797be63836cc52156ddf8f33cd72aac78866/lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31", size = 4999961, upload-time = "2025-09-22T04:00:17.619Z" }, + { url = "https://files.pythonhosted.org/packages/f5/55/d465e9b89df1761674d8672bb3e4ae2c47033b01ec243964b6e334c6743f/lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9", size = 5157087, upload-time = "2025-09-22T04:00:19.868Z" }, + { url = "https://files.pythonhosted.org/packages/62/38/3073cd7e3e8dfc3ba3c3a139e33bee3a82de2bfb0925714351ad3d255c13/lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8", size = 5067620, upload-time = "2025-09-22T04:00:21.877Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d3/1e001588c5e2205637b08985597827d3827dbaaece16348c8822bfe61c29/lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba", size = 5406664, upload-time = "2025-09-22T04:00:23.714Z" }, + { url = "https://files.pythonhosted.org/packages/20/cf/cab09478699b003857ed6ebfe95e9fb9fa3d3c25f1353b905c9b73cfb624/lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c", size = 5289397, upload-time = "2025-09-22T04:00:25.544Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/02a2d0c38ac9a8b9f9e5e1bbd3f24b3f426044ad618b552e9549ee91bd63/lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c", size = 4772178, upload-time = "2025-09-22T04:00:27.602Z" }, + { url = "https://files.pythonhosted.org/packages/56/87/e1ceadcc031ec4aa605fe95476892d0b0ba3b7f8c7dcdf88fdeff59a9c86/lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321", size = 5358148, upload-time = "2025-09-22T04:00:29.323Z" }, + { url = "https://files.pythonhosted.org/packages/fe/13/5bb6cf42bb228353fd4ac5f162c6a84fd68a4d6f67c1031c8cf97e131fc6/lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1", size = 5112035, upload-time = "2025-09-22T04:00:31.061Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e2/ea0498552102e59834e297c5c6dff8d8ded3db72ed5e8aad77871476f073/lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34", size = 4799111, upload-time = "2025-09-22T04:00:33.11Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9e/8de42b52a73abb8af86c66c969b3b4c2a96567b6ac74637c037d2e3baa60/lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a", size = 5351662, upload-time = "2025-09-22T04:00:35.237Z" }, + { url = "https://files.pythonhosted.org/packages/28/a2/de776a573dfb15114509a37351937c367530865edb10a90189d0b4b9b70a/lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c", size = 5314973, upload-time = "2025-09-22T04:00:37.086Z" }, + { url = "https://files.pythonhosted.org/packages/50/a0/3ae1b1f8964c271b5eec91db2043cf8c6c0bce101ebb2a633b51b044db6c/lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b", size = 3611953, upload-time = "2025-09-22T04:00:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/d1/70/bd42491f0634aad41bdfc1e46f5cff98825fb6185688dc82baa35d509f1a/lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0", size = 4032695, upload-time = "2025-09-22T04:00:41.402Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d0/05c6a72299f54c2c561a6c6cbb2f512e047fca20ea97a05e57931f194ac4/lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5", size = 3680051, upload-time = "2025-09-22T04:00:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, + { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9c/780c9a8fce3f04690b374f72f41306866b0400b9d0fdf3e17aaa37887eed/lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6", size = 3939264, upload-time = "2025-09-22T04:04:32.892Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5a/1ab260c00adf645d8bf7dec7f920f744b032f69130c681302821d5debea6/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba", size = 4216435, upload-time = "2025-09-22T04:04:34.907Z" }, + { url = "https://files.pythonhosted.org/packages/f2/37/565f3b3d7ffede22874b6d86be1a1763d00f4ea9fc5b9b6ccb11e4ec8612/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5", size = 4325913, upload-time = "2025-09-22T04:04:37.205Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/f3a1b169b2fb9d03467e2e3c0c752ea30e993be440a068b125fc7dd248b0/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4", size = 4269357, upload-time = "2025-09-22T04:04:39.322Z" }, + { url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d", size = 4412295, upload-time = "2025-09-22T04:04:41.647Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d", size = 3516913, upload-time = "2025-09-22T04:04:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markdownify" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/1b/6f2697b51eaca81f08852fd2734745af15718fea10222a1d40f8a239c4ea/markdownify-1.2.0.tar.gz", hash = "sha256:f6c367c54eb24ee953921804dfe6d6575c5e5b42c643955e7242034435de634c", size = 18771, upload-time = "2025-08-09T17:44:15.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/e2/7af643acb4cae0741dffffaa7f3f7c9e7ab4046724543ba1777c401d821c/markdownify-1.2.0-py3-none-any.whl", hash = "sha256:48e150a1c4993d4d50f282f725c0111bd9eb25645d41fa2f543708fd44161351", size = 15561, upload-time = "2025-08-09T17:44:14.074Z" }, +] + +[[package]] +name = "marshmallow" +version = "3.23.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/c0/d674c9de69227beafa41e1601b0c48b8b51060212abc231d4332e4b1e794/marshmallow-3.23.3.tar.gz", hash = "sha256:d586c8685ebdb80bf754e1f96e3f305aaf30951f1fc69175b977453633467e76", size = 175606, upload-time = "2025-01-03T20:18:41.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/82/d8c37cc92948ce11e5d8d71602bbac7ac4257f9e1f918fd91b1ddac4ec97/marshmallow-3.23.3-py3-none-any.whl", hash = "sha256:20c0f8c613f68bcb45b2a0d3282e2f172575560170bf220d67aafb42717910e4", size = 48911, upload-time = "2025-01-03T20:18:39.62Z" }, +] + +[[package]] +name = "mcp" +version = "1.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/54/dd2330ef4611c27ae59124820863c34e1d3edb1133c58e6375e2d938c9c5/mcp-1.21.0.tar.gz", hash = "sha256:bab0a38e8f8c48080d787233343f8d301b0e1e95846ae7dead251b2421d99855", size = 452697, upload-time = "2025-11-06T23:19:58.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/47/850b6edc96c03bd44b00de9a0ca3c1cc71e0ba1cd5822955bc9e4eb3fad3/mcp-1.21.0-py3-none-any.whl", hash = "sha256:598619e53eb0b7a6513db38c426b28a4bdf57496fed04332100d2c56acade98b", size = 173672, upload-time = "2025-11-06T23:19:56.508Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mistralai" +version = "1.9.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eval-type-backport" }, + { name = "httpx" }, + { name = "invoke" }, + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/8d/d8b7af67a966b6f227024e1cb7287fc19901a434f87a5a391dcfe635d338/mistralai-1.9.11.tar.gz", hash = "sha256:3df9e403c31a756ec79e78df25ee73cea3eb15f86693773e16b16adaf59c9b8a", size = 208051, upload-time = "2025-10-02T15:53:40.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/76/4ce12563aea5a76016f8643eff30ab731e6656c845e9e4d090ef10c7b925/mistralai-1.9.11-py3-none-any.whl", hash = "sha256:7a3dc2b8ef3fceaa3582220234261b5c4e3e03a972563b07afa150e44a25a6d3", size = 442796, upload-time = "2025-10-02T15:53:39.134Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/63/7bdd4adc330abcca54c85728db2327130e49e52e8c3ce685cec44e0f2e9f/multidict-6.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349", size = 77153, upload-time = "2025-10-06T14:48:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/b6c35ff175ed1a3142222b78455ee31be71a8396ed3ab5280fbe3ebe4e85/multidict-6.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e", size = 44993, upload-time = "2025-10-06T14:48:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1f/064c77877c5fa6df6d346e68075c0f6998547afe952d6471b4c5f6a7345d/multidict-6.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3", size = 44607, upload-time = "2025-10-06T14:48:29.581Z" }, + { url = "https://files.pythonhosted.org/packages/04/7a/bf6aa92065dd47f287690000b3d7d332edfccb2277634cadf6a810463c6a/multidict-6.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046", size = 241847, upload-time = "2025-10-06T14:48:32.107Z" }, + { url = "https://files.pythonhosted.org/packages/94/39/297a8de920f76eda343e4ce05f3b489f0ab3f9504f2576dfb37b7c08ca08/multidict-6.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32", size = 242616, upload-time = "2025-10-06T14:48:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/39/3a/d0eee2898cfd9d654aea6cb8c4addc2f9756e9a7e09391cfe55541f917f7/multidict-6.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73", size = 222333, upload-time = "2025-10-06T14:48:35.9Z" }, + { url = "https://files.pythonhosted.org/packages/05/48/3b328851193c7a4240815b71eea165b49248867bbb6153a0aee227a0bb47/multidict-6.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc", size = 253239, upload-time = "2025-10-06T14:48:37.302Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ca/0706a98c8d126a89245413225ca4a3fefc8435014de309cf8b30acb68841/multidict-6.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62", size = 251618, upload-time = "2025-10-06T14:48:38.963Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/9c7992f245554d8b173f6f0a048ad24b3e645d883f096857ec2c0822b8bd/multidict-6.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84", size = 241655, upload-time = "2025-10-06T14:48:40.312Z" }, + { url = "https://files.pythonhosted.org/packages/31/79/26a85991ae67efd1c0b1fc2e0c275b8a6aceeb155a68861f63f87a798f16/multidict-6.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0", size = 239245, upload-time = "2025-10-06T14:48:41.848Z" }, + { url = "https://files.pythonhosted.org/packages/14/1e/75fa96394478930b79d0302eaf9a6c69f34005a1a5251ac8b9c336486ec9/multidict-6.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e", size = 233523, upload-time = "2025-10-06T14:48:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5e/085544cb9f9c4ad2b5d97467c15f856df8d9bac410cffd5c43991a5d878b/multidict-6.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4", size = 243129, upload-time = "2025-10-06T14:48:45.225Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c3/e9d9e2f20c9474e7a8fcef28f863c5cbd29bb5adce6b70cebe8bdad0039d/multidict-6.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648", size = 248999, upload-time = "2025-10-06T14:48:46.703Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3f/df171b6efa3239ae33b97b887e42671cd1d94d460614bfb2c30ffdab3b95/multidict-6.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111", size = 243711, upload-time = "2025-10-06T14:48:48.146Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2f/9b5564888c4e14b9af64c54acf149263721a283aaf4aa0ae89b091d5d8c1/multidict-6.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36", size = 237504, upload-time = "2025-10-06T14:48:49.447Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3a/0bd6ca0f7d96d790542d591c8c3354c1e1b6bfd2024d4d92dc3d87485ec7/multidict-6.7.0-cp310-cp310-win32.whl", hash = "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85", size = 41422, upload-time = "2025-10-06T14:48:50.789Z" }, + { url = "https://files.pythonhosted.org/packages/00/35/f6a637ea2c75f0d3b7c7d41b1189189acff0d9deeb8b8f35536bb30f5e33/multidict-6.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7", size = 46050, upload-time = "2025-10-06T14:48:51.938Z" }, + { url = "https://files.pythonhosted.org/packages/e7/b8/f7bf8329b39893d02d9d95cf610c75885d12fc0f402b1c894e1c8e01c916/multidict-6.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0", size = 43153, upload-time = "2025-10-06T14:48:53.146Z" }, + { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, + { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, + { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, + { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, + { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, + { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, + { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, + { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, + { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, + { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, + { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, + { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, + { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, + { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, + { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, + { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, + { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, + { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, + { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, + { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, + { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, + { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, + { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, + { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, + { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, + { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, + { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, + { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, + { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, + { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, + { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, + { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, + { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, + { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, + { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, + { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, + { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, + { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +] + +[[package]] +name = "mypy" +version = "1.18.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" }, + { url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" }, + { url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4f/90dc8c15c1441bf31cf0f9918bb077e452618708199e530f4cbd5cede6ff/mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed", size = 9766753, upload-time = "2025-09-19T00:10:49.161Z" }, + { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, + { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, + { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, + { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, + { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, + { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, + { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nexus-rpc" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/66/540687556bd28cf1ec370cc6881456203dfddb9dab047b8979c6865b5984/nexus_rpc-1.1.0.tar.gz", hash = "sha256:d65ad6a2f54f14e53ebe39ee30555eaeb894102437125733fb13034a04a44553", size = 77383, upload-time = "2025-07-07T19:03:58.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/2f/9e9d0dcaa4c6ffa22b7aa31069a8a264c753ff8027b36af602cce038c92f/nexus_rpc-1.1.0-py3-none-any.whl", hash = "sha256:d1b007af2aba186a27e736f8eaae39c03aed05b488084ff6c3d1785c9ba2ad38", size = 27743, upload-time = "2025-07-07T19:03:57.556Z" }, +] + +[[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, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "numpy" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411, upload-time = "2024-02-05T23:48:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016, upload-time = "2024-02-05T23:48:54.098Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746, upload-time = "2024-02-05T23:49:51.983Z" }, + { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659, upload-time = "2024-02-05T23:50:35.834Z" }, + { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" }, + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, +] + +[[package]] +name = "openai" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/dd/4d4d46a06943e37c95b6e388237e1e38d1e9aab264ff070f86345d60b7a4/openai-2.1.0.tar.gz", hash = "sha256:47f3463a5047340a989b4c0cd5378054acfca966ff61a96553b22f098e3270a2", size = 572998, upload-time = "2025-10-02T20:43:15.385Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/83/88f64fc8f037885efa8a629d1215f5bc1f037453bab4d4f823b5533319eb/openai-2.1.0-py3-none-any.whl", hash = "sha256:33172e8c06a4576144ba4137a493807a9ca427421dcabc54ad3aa656daf757d3", size = 964939, upload-time = "2025-10-02T20:43:13.568Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242, upload-time = "2025-10-16T08:35:50.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947, upload-time = "2025-10-16T08:35:30.23Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/83/dd4660f2956ff88ed071e9e0e36e830df14b8c5dc06722dbde1841accbe8/opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c", size = 20431, upload-time = "2025-10-16T08:35:53.285Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359, upload-time = "2025-10-16T08:35:34.099Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/0a/debcdfb029fbd1ccd1563f7c287b89a6f7bef3b2902ade56797bfd020854/opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b", size = 17282, upload-time = "2025-10-16T08:35:54.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/77/154004c99fb9f291f74aa0822a2f5bbf565a72d8126b3a1b63ed8e5f83c7/opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b", size = 19579, upload-time = "2025-10-16T08:35:36.269Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/ed/9c65cd209407fd807fa05be03ee30f159bdac8d59e7ea16a8fe5a1601222/opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc", size = 31544, upload-time = "2025-10-16T08:39:31.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/f5/7a40ff3f62bfe715dad2f633d7f1174ba1a7dd74254c15b2558b3401262a/opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee", size = 33020, upload-time = "2025-10-16T08:38:31.463Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-httpx" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/6b/1bdf36b68cace9b4eae3cbbade4150c71c90aa392b127dda5bb5c2a49307/opentelemetry_instrumentation_httpx-0.59b0.tar.gz", hash = "sha256:a1cb9b89d9f05a82701cc9ab9cfa3db54fd76932489449778b350bc1b9f0e872", size = 19886, upload-time = "2025-10-16T08:39:48.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/16/c1e0745d20af392ec9060693531d7f01239deb2d81e460d0c379719691b8/opentelemetry_instrumentation_httpx-0.59b0-py3-none-any.whl", hash = "sha256:7dc9f66aef4ca3904d877f459a70c78eafd06131dc64d713b9b1b5a7d0a48f05", size = 15197, upload-time = "2025-10-16T08:38:55.507Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/14/f0c4f0f6371b9cb7f9fa9ee8918bfd59ac7040c7791f1e6da32a1839780d/opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468", size = 46152, upload-time = "2025-10-16T08:36:01.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/6a/82b68b14efca5150b2632f3692d627afa76b77378c4999f2648979409528/opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18", size = 72535, upload-time = "2025-10-16T08:35:45.749Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942, upload-time = "2025-10-16T08:36:02.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349, upload-time = "2025-10-16T08:35:46.995Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861, upload-time = "2025-10-16T08:36:03.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954, upload-time = "2025-10-16T08:35:48.054Z" }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/f7/13cd081e7851c42520ab0e96efb17ffbd901111a50b8252ec1e240664020/opentelemetry_util_http-0.59b0.tar.gz", hash = "sha256:ae66ee91be31938d832f3b4bc4eb8a911f6eddd38969c4a871b1230db2a0a560", size = 9412, upload-time = "2025-10-16T08:40:11.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/56/62282d1d4482061360449dacc990c89cad0fc810a2ed937b636300f55023/opentelemetry_util_http-0.59b0-py3-none-any.whl", hash = "sha256:6d036a07563bce87bf521839c0671b507a02a0d39d7ea61b88efa14c6e25355d", size = 7648, upload-time = "2025-10-16T08:39:25.706Z" }, +] + +[[package]] +name = "outcome" +version = "1.3.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" }, +] + +[[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, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.4.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/a6/49/7845c2d7bf6474efd8e27905b51b11e6ce411708c91e829b93f324de9929/pre_commit-4.4.0.tar.gz", hash = "sha256:f0233ebab440e9f17cabbb558706eb173d19ace965c68cdce2c081042b4fab15", size = 197501, upload-time = "2025-11-08T21:12:11.607Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/11/574fe7d13acf30bfd0a8dd7fa1647040f2b8064f13f43e8c963b1e65093b/pre_commit-4.4.0-py2.py3-none-any.whl", hash = "sha256:b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813", size = 226049, upload-time = "2025-11-08T21:12:10.228Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/ff/64a6c8f420818bb873713988ca5492cba3a7946be57e027ac63495157d97/protobuf-6.33.0.tar.gz", hash = "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954", size = 443463, upload-time = "2025-10-15T20:39:52.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/ee/52b3fa8feb6db4a833dfea4943e175ce645144532e8a90f72571ad85df4e/protobuf-6.33.0-cp310-abi3-win32.whl", hash = "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035", size = 425593, upload-time = "2025-10-15T20:39:40.29Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c6/7a465f1825872c55e0341ff4a80198743f73b69ce5d43ab18043699d1d81/protobuf-6.33.0-cp310-abi3-win_amd64.whl", hash = "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee", size = 436882, upload-time = "2025-10-15T20:39:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a9/b6eee662a6951b9c3640e8e452ab3e09f117d99fc10baa32d1581a0d4099/protobuf-6.33.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455", size = 427521, upload-time = "2025-10-15T20:39:43.803Z" }, + { url = "https://files.pythonhosted.org/packages/10/35/16d31e0f92c6d2f0e77c2a3ba93185130ea13053dd16200a57434c882f2b/protobuf-6.33.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90", size = 324445, upload-time = "2025-10-15T20:39:44.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/2a981a13e35cda8b75b5585aaffae2eb904f8f351bdd3870769692acbd8a/protobuf-6.33.0-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298", size = 339159, upload-time = "2025-10-15T20:39:46.186Z" }, + { url = "https://files.pythonhosted.org/packages/21/51/0b1cbad62074439b867b4e04cc09b93f6699d78fd191bed2bbb44562e077/protobuf-6.33.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef", size = 323172, upload-time = "2025-10-15T20:39:47.465Z" }, + { url = "https://files.pythonhosted.org/packages/07/d1/0a28c21707807c6aacd5dc9c3704b2aa1effbf37adebd8caeaf68b17a636/protobuf-6.33.0-py3-none-any.whl", hash = "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995", size = 170477, upload-time = "2025-10-15T20:39:51.311Z" }, +] + +[[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, upload-time = "2024-09-10T22:41:42.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[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, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-ai" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "vertexai"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/24/41b74d1348d8978f5c092a8bba7ac5775aa4bd6aab454c121232dfef6e8d/pydantic_ai-1.5.0.tar.gz", hash = "sha256:e16bda3c6e0c117f16013c654b4bcff04efb2d8482a9a220926d1faed37902ae", size = 100380578, upload-time = "2025-10-24T15:49:42.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c4/8747694f913f5befd41e0054388f311bf8126623a27d3b56970cf4e80ea4/pydantic_ai-1.5.0-py3-none-any.whl", hash = "sha256:f6d9a3741edfb8a59789fd7b30a56917326e5f7d26c2a84e8fecf8fca99bac82", size = 11710, upload-time = "2025-10-24T15:49:33.478Z" }, +] + +[[package]] +name = "pydantic-ai-slim" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "genai-prices" }, + { name = "griffe" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pydantic-graph" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/66/41d5b7d09a873a1f056a493b473b39788bc6586b66dcce34b56a61f6c853/pydantic_ai_slim-1.5.0.tar.gz", hash = "sha256:e75f9fecde384296f7cd5ce47b320f96df893395f4d6542ed03c43424a3cc2e2", size = 277995, upload-time = "2025-10-24T15:49:47.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/0e/fb386715fa2e84e5c78b76a524d9551bbf74f5b0839564a8fd8abf393efc/pydantic_ai_slim-1.5.0-py3-none-any.whl", hash = "sha256:5c896dd7e2b7945a339565441269a10fb5103501ca11ae18acb0f72ace2e535e", size = 367308, upload-time = "2025-10-24T15:49:36.177Z" }, +] + +[package.optional-dependencies] +ag-ui = [ + { name = "ag-ui-protocol" }, + { name = "starlette" }, +] +anthropic = [ + { name = "anthropic" }, +] +bedrock = [ + { name = "boto3" }, +] +cli = [ + { name = "argcomplete" }, + { name = "prompt-toolkit" }, + { name = "pyperclip" }, + { name = "rich" }, +] +cohere = [ + { name = "cohere", marker = "sys_platform != 'emscripten'" }, +] +evals = [ + { name = "pydantic-evals" }, +] +google = [ + { name = "google-genai" }, +] +groq = [ + { name = "groq" }, +] +huggingface = [ + { name = "huggingface-hub" }, +] +logfire = [ + { name = "logfire", extra = ["httpx"] }, +] +mcp = [ + { name = "mcp" }, +] +mistral = [ + { name = "mistralai" }, +] +openai = [ + { name = "openai" }, +] +retries = [ + { name = "tenacity" }, +] +temporal = [ + { name = "temporalio" }, +] +vertexai = [ + { name = "google-auth" }, + { name = "requests" }, +] + +[[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, upload-time = "2025-04-23T18:33:52.104Z" } +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, upload-time = "2025-04-23T18:30:43.919Z" }, + { 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, upload-time = "2025-04-23T18:30:46.372Z" }, + { 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, upload-time = "2025-04-23T18:30:47.591Z" }, + { 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, upload-time = "2025-04-23T18:30:49.328Z" }, + { 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, upload-time = "2025-04-23T18:30:50.907Z" }, + { 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, upload-time = "2025-04-23T18:30:52.083Z" }, + { 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, upload-time = "2025-04-23T18:30:53.389Z" }, + { 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, upload-time = "2025-04-23T18:30:54.661Z" }, + { 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, upload-time = "2025-04-23T18:30:56.11Z" }, + { 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, upload-time = "2025-04-23T18:30:57.501Z" }, + { 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, upload-time = "2025-04-23T18:30:58.867Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, + { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, + { 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, upload-time = "2025-04-23T18:31:03.106Z" }, + { 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, upload-time = "2025-04-23T18:31:04.621Z" }, + { 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, upload-time = "2025-04-23T18:31:06.377Z" }, + { 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, upload-time = "2025-04-23T18:31:07.93Z" }, + { 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, upload-time = "2025-04-23T18:31:09.283Z" }, + { 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, upload-time = "2025-04-23T18:31:11.7Z" }, + { 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, upload-time = "2025-04-23T18:31:13.536Z" }, + { 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, upload-time = "2025-04-23T18:31:15.011Z" }, + { 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, upload-time = "2025-04-23T18:31:16.393Z" }, + { 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, upload-time = "2025-04-23T18:31:17.892Z" }, + { 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, upload-time = "2025-04-23T18:31:19.205Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, + { 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, upload-time = "2025-04-23T18:31:25.863Z" }, + { 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, upload-time = "2025-04-23T18:31:27.341Z" }, + { 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, upload-time = "2025-04-23T18:31:28.956Z" }, + { 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, upload-time = "2025-04-23T18:31:31.025Z" }, + { 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, upload-time = "2025-04-23T18:31:32.514Z" }, + { 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, upload-time = "2025-04-23T18:31:33.958Z" }, + { 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, upload-time = "2025-04-23T18:31:39.095Z" }, + { 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, upload-time = "2025-04-23T18:31:41.034Z" }, + { 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, upload-time = "2025-04-23T18:31:42.757Z" }, + { 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, upload-time = "2025-04-23T18:31:44.304Z" }, + { 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, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { 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, upload-time = "2025-04-23T18:31:53.175Z" }, + { 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, upload-time = "2025-04-23T18:31:54.79Z" }, + { 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, upload-time = "2025-04-23T18:31:57.393Z" }, + { 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, upload-time = "2025-04-23T18:31:59.065Z" }, + { 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, upload-time = "2025-04-23T18:32:00.78Z" }, + { 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, upload-time = "2025-04-23T18:32:02.418Z" }, + { 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, upload-time = "2025-04-23T18:32:04.152Z" }, + { 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, upload-time = "2025-04-23T18:32:06.129Z" }, + { 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, upload-time = "2025-04-23T18:32:08.178Z" }, + { 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, upload-time = "2025-04-23T18:32:10.242Z" }, + { 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, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { 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, upload-time = "2025-04-23T18:32:20.188Z" }, + { 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, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, + { 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, upload-time = "2025-04-23T18:32:53.14Z" }, + { 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, upload-time = "2025-04-23T18:32:55.52Z" }, + { 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, upload-time = "2025-04-23T18:32:57.546Z" }, + { 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, upload-time = "2025-04-23T18:32:59.771Z" }, + { 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, upload-time = "2025-04-23T18:33:04.51Z" }, + { 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, upload-time = "2025-04-23T18:33:06.391Z" }, + { 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, upload-time = "2025-04-23T18:33:08.44Z" }, + { 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, upload-time = "2025-04-23T18:33:10.313Z" }, + { 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, upload-time = "2025-04-23T18:33:12.224Z" }, + { 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, upload-time = "2025-04-23T18:33:14.199Z" }, + { 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, upload-time = "2025-04-23T18:33:16.555Z" }, + { 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, upload-time = "2025-04-23T18:33:18.513Z" }, + { 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, upload-time = "2025-04-23T18:33:20.475Z" }, + { 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, upload-time = "2025-04-23T18:33:22.501Z" }, + { 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, upload-time = "2025-04-23T18:33:24.528Z" }, + { 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, upload-time = "2025-04-23T18:33:26.621Z" }, + { 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, upload-time = "2025-04-23T18:33:28.656Z" }, + { 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, upload-time = "2025-04-23T18:33:30.645Z" }, +] + +[[package]] +name = "pydantic-evals" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "pydantic-ai-slim" }, + { name = "pyyaml" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/09/bc9f99eca6e77f2230dc9df04b31c23121062039654ac491b318e56fdbe1/pydantic_evals-1.5.0.tar.gz", hash = "sha256:f5b1e6606d152b211e4b59eee10a49d976e0f092030cc3f7b86441e713b3a1c6", size = 45930, upload-time = "2025-10-24T15:49:48.936Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/f8/c62ec30e70d8cc54dacdd338c861e39b6018a68884ead4b1b385fbc466d0/pydantic_evals-1.5.0-py3-none-any.whl", hash = "sha256:02b833b1e239ff18a70fcb1209812f282ba59f0cb7830badf7324e078ccbb888", size = 55020, upload-time = "2025-10-24T15:49:38.23Z" }, +] + +[[package]] +name = "pydantic-graph" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/ba/94aa12dc08c50a0d9c293f5558ec008909562a429b2436b014465d4419a1/pydantic_graph-1.5.0.tar.gz", hash = "sha256:747802da8603c83254fb32ec3ec1a0fa1c085e3655521af3fc57c1b2335e2ab9", size = 56887, upload-time = "2025-10-24T15:49:49.781Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/3c/40fcba26438a87a5c55a9b64a1f05cbaadc70be775ff56baf44e5e3a1c79/pydantic_graph-1.5.0-py3-none-any.whl", hash = "sha256:d850955c5273bd91b8e540a965139fa07dfb093fa9e963f5df554b5edbd74836", size = 70858, upload-time = "2025-10-24T15:49:39.419Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, +] + +[[package]] +name = "pydriller" +version = "2.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitpython" }, + { name = "lizard" }, + { name = "pytz" }, + { name = "types-pytz" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/c4/5628ff7e934621473a024b668f9081a641fcf607303783a7160c700b6497/PyDriller-2.9-py3-none-any.whl", hash = "sha256:bc4e72ff59ce62f1ee4dec77e44103df62b62b6d5ed90daea75abe2eb16c6ebf", size = 36811, upload-time = "2025-09-06T07:40:12.771Z" }, +] + +[[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, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[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, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pyld" +version = "2.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "frozendict" }, + { name = "lxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/0b/d97dddcc079d4961aa38bec1ad444b8a3e39ea0fd5627682cac25d452c82/PyLD-2.0.4.tar.gz", hash = "sha256:311e350f0dbc964311c79c28e86f84e195a81d06fef5a6f6ac2a4f6391ceeacc", size = 70976, upload-time = "2024-02-16T17:35:51.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/cd/80760be197a4bd08e7c136ef4bcb4a2c63fc799d8d91f4c177b21183135e/PyLD-2.0.4-py3-none-any.whl", hash = "sha256:6dab9905644616df33f8755489fc9b354ed7d832d387b7d1974b4fbd3b8d2a89", size = 70868, upload-time = "2024-02-16T17:35:49Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.2.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, +] + +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + +[[package]] +name = "pysocks" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.0" +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/da/1d/eb34f286b164c5e431a810a38697409cca1112cee04b287bb56ac486730b/pytest-9.0.0.tar.gz", hash = "sha256:8f44522eafe4137b0f35c9ce3072931a788a21ee40a2ed279e817d3cc16ed21e", size = 1562764, upload-time = "2025-11-08T17:25:33.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/99/cafef234114a3b6d9f3aaed0723b437c40c57bdb7b3e4c3a575bc4890052/pytest-9.0.0-py3-none-any.whl", hash = "sha256:e5ccdf10b0bac554970ee88fc1a4ad0ee5d221f8ef22321f9b7e4584e19d7f96", size = 373364, upload-time = "2025-11-08T17:25:31.811Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/d7/d548e0d5a68b328a8d69af833a861be415a17cb15ce3d8f0cd850073d2e1/python-dotenv-0.21.1.tar.gz", hash = "sha256:1c93de8f636cde3ce377292818d0e440b6e45a82f215c3744979151fa8151c49", size = 35930, upload-time = "2023-01-21T10:22:47.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/62/f19d1e9023aacb47241de3ab5a5d5fedf32c78a71a9e365bb2153378c141/python_dotenv-0.21.1-py3-none-any.whl", hash = "sha256:41e12e0318bebc859fcc4d97d4db8d20ad21721a6aa5047dd59f090391cb549a", size = 19284, upload-time = "2023-01-21T10:22:45.958Z" }, +] + +[[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, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pytokens" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/a762be14dae1c3bf280202ba3172020b2b0b4c537f94427435f19c413b72/pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a", size = 17644, upload-time = "2025-11-05T13:36:35.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" }, +] + +[[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, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.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, upload-time = "2024-08-06T20:33:50.674Z" } +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, upload-time = "2024-08-06T20:31:40.178Z" }, + { 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, upload-time = "2024-08-06T20:31:42.173Z" }, + { 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, upload-time = "2024-08-06T20:31:44.263Z" }, + { 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, upload-time = "2024-08-06T20:31:50.199Z" }, + { 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, upload-time = "2024-08-06T20:31:52.292Z" }, + { 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, upload-time = "2024-08-06T20:31:53.836Z" }, + { 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, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { 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, upload-time = "2024-08-06T20:32:03.408Z" }, + { 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, upload-time = "2024-08-06T20:32:04.926Z" }, + { 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, upload-time = "2024-08-06T20:32:06.459Z" }, + { 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, upload-time = "2024-08-06T20:32:08.338Z" }, + { 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, upload-time = "2024-08-06T20:32:14.124Z" }, + { 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, upload-time = "2024-08-06T20:32:16.17Z" }, + { 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, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { 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, upload-time = "2024-08-06T20:32:25.131Z" }, + { 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, upload-time = "2024-08-06T20:32:26.511Z" }, + { 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, upload-time = "2024-08-06T20:32:28.363Z" }, + { 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, upload-time = "2024-08-06T20:32:30.058Z" }, + { 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, upload-time = "2024-08-06T20:32:31.881Z" }, + { 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, upload-time = "2024-08-06T20:32:37.083Z" }, + { 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, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { 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, upload-time = "2024-08-06T20:32:43.4Z" }, + { 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, upload-time = "2024-08-06T20:32:44.801Z" }, + { 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, upload-time = "2024-08-06T20:32:46.432Z" }, + { 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, upload-time = "2024-08-06T20:32:51.188Z" }, + { 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, upload-time = "2024-08-06T20:32:53.019Z" }, + { 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, upload-time = "2024-08-06T20:32:54.708Z" }, + { 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, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "rdflib" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "isodate" }, + { name = "pyparsing" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/8d/2d1c8a08471b4333657c98a3048642095f844f10cd1d4e28f9b08725c7bd/rdflib-6.2.0.tar.gz", hash = "sha256:62dc3c86d1712db0f55785baf8047f63731fa59b2682be03219cb89262065942", size = 4755909, upload-time = "2022-07-26T15:43:59.891Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/fb/a0f8b6ab6598b49871a48a189dc1942fb0b0543ab4c84f689486233ef1ec/rdflib-6.2.0-py3-none-any.whl", hash = "sha256:85c34a86dfc517a41e5f2425a41a0aceacc23983462b32e68610b9fad1383bca", size = 500261, upload-time = "2022-07-26T15:43:56.513Z" }, +] + +[[package]] +name = "rdflib-jsonld" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rdflib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/1a/627de985dffc11b486eb07be86dc9a16c25b4877905f5f6a0be3633addb0/rdflib-jsonld-0.6.2.tar.gz", hash = "sha256:107cd3019d41354c31687e64af5e3fd3c3e3fa5052ce635f5ce595fd31853a63", size = 12449, upload-time = "2021-09-18T03:04:27.881Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/92/da92898b2aab0da78207afc9c035a71bedef3544966374c44e9627d761c5/rdflib_jsonld-0.6.2-py2.py3-none-any.whl", hash = "sha256:011afe67672353ca9978ab9a4bee964dff91f14042f2d8a28c22a573779d2f8b", size = 4029, upload-time = "2021-09-18T03:04:26.34Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2025.11.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/d6/d788d52da01280a30a3f6268aef2aa71043bff359c618fea4c5b536654d5/regex-2025.11.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2b441a4ae2c8049106e8b39973bfbddfb25a179dda2bdb99b0eeb60c40a6a3af", size = 488087, upload-time = "2025-11-03T21:30:47.317Z" }, + { url = "https://files.pythonhosted.org/packages/69/39/abec3bd688ec9bbea3562de0fd764ff802976185f5ff22807bf0a2697992/regex-2025.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2fa2eed3f76677777345d2f81ee89f5de2f5745910e805f7af7386a920fa7313", size = 290544, upload-time = "2025-11-03T21:30:49.912Z" }, + { url = "https://files.pythonhosted.org/packages/39/b3/9a231475d5653e60002508f41205c61684bb2ffbf2401351ae2186897fc4/regex-2025.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8b4a27eebd684319bdf473d39f1d79eed36bf2cd34bd4465cdb4618d82b3d56", size = 288408, upload-time = "2025-11-03T21:30:51.344Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c5/1929a0491bd5ac2d1539a866768b88965fa8c405f3e16a8cef84313098d6/regex-2025.11.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cf77eac15bd264986c4a2c63353212c095b40f3affb2bc6b4ef80c4776c1a28", size = 781584, upload-time = "2025-11-03T21:30:52.596Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fd/16aa16cf5d497ef727ec966f74164fbe75d6516d3d58ac9aa989bc9cdaad/regex-2025.11.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f9ee819f94c6abfa56ec7b1dbab586f41ebbdc0a57e6524bd5e7f487a878c7", size = 850733, upload-time = "2025-11-03T21:30:53.825Z" }, + { url = "https://files.pythonhosted.org/packages/e6/49/3294b988855a221cb6565189edf5dc43239957427df2d81d4a6b15244f64/regex-2025.11.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:838441333bc90b829406d4a03cb4b8bf7656231b84358628b0406d803931ef32", size = 898691, upload-time = "2025-11-03T21:30:55.575Z" }, + { url = "https://files.pythonhosted.org/packages/14/62/b56d29e70b03666193369bdbdedfdc23946dbe9f81dd78ce262c74d988ab/regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe6d3f0c9e3b7e8c0c694b24d25e677776f5ca26dce46fd6b0489f9c8339391", size = 791662, upload-time = "2025-11-03T21:30:57.262Z" }, + { url = "https://files.pythonhosted.org/packages/15/fc/e4c31d061eced63fbf1ce9d853975f912c61a7d406ea14eda2dd355f48e7/regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2ab815eb8a96379a27c3b6157fcb127c8f59c36f043c1678110cea492868f1d5", size = 782587, upload-time = "2025-11-03T21:30:58.788Z" }, + { url = "https://files.pythonhosted.org/packages/b2/bb/5e30c7394bcf63f0537121c23e796be67b55a8847c3956ae6068f4c70702/regex-2025.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:728a9d2d173a65b62bdc380b7932dd8e74ed4295279a8fe1021204ce210803e7", size = 774709, upload-time = "2025-11-03T21:31:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c4/fce773710af81b0cb37cb4ff0947e75d5d17dee304b93d940b87a67fc2f4/regex-2025.11.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:509dc827f89c15c66a0c216331260d777dd6c81e9a4e4f830e662b0bb296c313", size = 845773, upload-time = "2025-11-03T21:31:01.583Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5e/9466a7ec4b8ec282077095c6eb50a12a389d2e036581134d4919e8ca518c/regex-2025.11.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:849202cd789e5f3cf5dcc7822c34b502181b4824a65ff20ce82da5524e45e8e9", size = 836164, upload-time = "2025-11-03T21:31:03.244Z" }, + { url = "https://files.pythonhosted.org/packages/95/18/82980a60e8ed1594eb3c89eb814fb276ef51b9af7caeab1340bfd8564af6/regex-2025.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b6f78f98741dcc89607c16b1e9426ee46ce4bf31ac5e6b0d40e81c89f3481ea5", size = 779832, upload-time = "2025-11-03T21:31:04.876Z" }, + { url = "https://files.pythonhosted.org/packages/03/cc/90ab0fdbe6dce064a42015433f9152710139fb04a8b81b4fb57a1cb63ffa/regex-2025.11.3-cp310-cp310-win32.whl", hash = "sha256:149eb0bba95231fb4f6d37c8f760ec9fa6fabf65bab555e128dde5f2475193ec", size = 265802, upload-time = "2025-11-03T21:31:06.581Z" }, + { url = "https://files.pythonhosted.org/packages/34/9d/e9e8493a85f3b1ddc4a5014465f5c2b78c3ea1cbf238dcfde78956378041/regex-2025.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:ee3a83ce492074c35a74cc76cf8235d49e77b757193a5365ff86e3f2f93db9fd", size = 277722, upload-time = "2025-11-03T21:31:08.144Z" }, + { url = "https://files.pythonhosted.org/packages/15/c4/b54b24f553966564506dbf873a3e080aef47b356a3b39b5d5aba992b50db/regex-2025.11.3-cp310-cp310-win_arm64.whl", hash = "sha256:38af559ad934a7b35147716655d4a2f79fcef2d695ddfe06a06ba40ae631fa7e", size = 270289, upload-time = "2025-11-03T21:31:10.267Z" }, + { url = "https://files.pythonhosted.org/packages/f7/90/4fb5056e5f03a7048abd2b11f598d464f0c167de4f2a51aa868c376b8c70/regex-2025.11.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eadade04221641516fa25139273505a1c19f9bf97589a05bc4cfcd8b4a618031", size = 488081, upload-time = "2025-11-03T21:31:11.946Z" }, + { url = "https://files.pythonhosted.org/packages/85/23/63e481293fac8b069d84fba0299b6666df720d875110efd0338406b5d360/regex-2025.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feff9e54ec0dd3833d659257f5c3f5322a12eee58ffa360984b716f8b92983f4", size = 290554, upload-time = "2025-11-03T21:31:13.387Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9d/b101d0262ea293a0066b4522dfb722eb6a8785a8c3e084396a5f2c431a46/regex-2025.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b30bc921d50365775c09a7ed446359e5c0179e9e2512beec4a60cbcef6ddd50", size = 288407, upload-time = "2025-11-03T21:31:14.809Z" }, + { url = "https://files.pythonhosted.org/packages/0c/64/79241c8209d5b7e00577ec9dca35cd493cc6be35b7d147eda367d6179f6d/regex-2025.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f99be08cfead2020c7ca6e396c13543baea32343b7a9a5780c462e323bd8872f", size = 793418, upload-time = "2025-11-03T21:31:16.556Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e2/23cd5d3573901ce8f9757c92ca4db4d09600b865919b6d3e7f69f03b1afd/regex-2025.11.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6dd329a1b61c0ee95ba95385fb0c07ea0d3fe1a21e1349fa2bec272636217118", size = 860448, upload-time = "2025-11-03T21:31:18.12Z" }, + { url = "https://files.pythonhosted.org/packages/2a/4c/aecf31beeaa416d0ae4ecb852148d38db35391aac19c687b5d56aedf3a8b/regex-2025.11.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c5238d32f3c5269d9e87be0cf096437b7622b6920f5eac4fd202468aaeb34d2", size = 907139, upload-time = "2025-11-03T21:31:20.753Z" }, + { url = "https://files.pythonhosted.org/packages/61/22/b8cb00df7d2b5e0875f60628594d44dba283e951b1ae17c12f99e332cc0a/regex-2025.11.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10483eefbfb0adb18ee9474498c9a32fcf4e594fbca0543bb94c48bac6183e2e", size = 800439, upload-time = "2025-11-03T21:31:22.069Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/c4b20330a5cdc7a8eb265f9ce593f389a6a88a0c5f280cf4d978f33966bc/regex-2025.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78c2d02bb6e1da0720eedc0bad578049cad3f71050ef8cd065ecc87691bed2b0", size = 782965, upload-time = "2025-11-03T21:31:23.598Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4c/ae3e52988ae74af4b04d2af32fee4e8077f26e51b62ec2d12d246876bea2/regex-2025.11.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b49cd2aad93a1790ce9cffb18964f6d3a4b0b3dbdbd5de094b65296fce6e58", size = 854398, upload-time = "2025-11-03T21:31:25.008Z" }, + { url = "https://files.pythonhosted.org/packages/06/d1/a8b9cf45874eda14b2e275157ce3b304c87e10fb38d9fc26a6e14eb18227/regex-2025.11.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:885b26aa3ee56433b630502dc3d36ba78d186a00cc535d3806e6bfd9ed3c70ab", size = 845897, upload-time = "2025-11-03T21:31:26.427Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fe/1830eb0236be93d9b145e0bd8ab499f31602fe0999b1f19e99955aa8fe20/regex-2025.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddd76a9f58e6a00f8772e72cff8ebcff78e022be95edf018766707c730593e1e", size = 788906, upload-time = "2025-11-03T21:31:28.078Z" }, + { url = "https://files.pythonhosted.org/packages/66/47/dc2577c1f95f188c1e13e2e69d8825a5ac582ac709942f8a03af42ed6e93/regex-2025.11.3-cp311-cp311-win32.whl", hash = "sha256:3e816cc9aac1cd3cc9a4ec4d860f06d40f994b5c7b4d03b93345f44e08cc68bf", size = 265812, upload-time = "2025-11-03T21:31:29.72Z" }, + { url = "https://files.pythonhosted.org/packages/50/1e/15f08b2f82a9bbb510621ec9042547b54d11e83cb620643ebb54e4eb7d71/regex-2025.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:087511f5c8b7dfbe3a03f5d5ad0c2a33861b1fc387f21f6f60825a44865a385a", size = 277737, upload-time = "2025-11-03T21:31:31.422Z" }, + { url = "https://files.pythonhosted.org/packages/f4/fc/6500eb39f5f76c5e47a398df82e6b535a5e345f839581012a418b16f9cc3/regex-2025.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:1ff0d190c7f68ae7769cd0313fe45820ba07ffebfddfaa89cc1eb70827ba0ddc", size = 270290, upload-time = "2025-11-03T21:31:33.041Z" }, + { url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312, upload-time = "2025-11-03T21:31:34.343Z" }, + { url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256, upload-time = "2025-11-03T21:31:35.675Z" }, + { url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921, upload-time = "2025-11-03T21:31:37.07Z" }, + { url = "https://files.pythonhosted.org/packages/ea/98/6a8dff667d1af907150432cf5abc05a17ccd32c72a3615410d5365ac167a/regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", size = 798568, upload-time = "2025-11-03T21:31:38.784Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/92c1db4fa4e12733dd5a526c2dd2b6edcbfe13257e135fc0f6c57f34c173/regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", size = 864165, upload-time = "2025-11-03T21:31:40.559Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e7/3ad7da8cdee1ce66c7cd37ab5ab05c463a86ffeb52b1a25fe7bd9293b36c/regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", size = 912182, upload-time = "2025-11-03T21:31:42.002Z" }, + { url = "https://files.pythonhosted.org/packages/84/bd/9ce9f629fcb714ffc2c3faf62b6766ecb7a585e1e885eb699bcf130a5209/regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", size = 803501, upload-time = "2025-11-03T21:31:43.815Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0f/8dc2e4349d8e877283e6edd6c12bdcebc20f03744e86f197ab6e4492bf08/regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", size = 787842, upload-time = "2025-11-03T21:31:45.353Z" }, + { url = "https://files.pythonhosted.org/packages/f9/73/cff02702960bc185164d5619c0c62a2f598a6abff6695d391b096237d4ab/regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", size = 858519, upload-time = "2025-11-03T21:31:46.814Z" }, + { url = "https://files.pythonhosted.org/packages/61/83/0e8d1ae71e15bc1dc36231c90b46ee35f9d52fab2e226b0e039e7ea9c10a/regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", size = 850611, upload-time = "2025-11-03T21:31:48.289Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f5/70a5cdd781dcfaa12556f2955bf170cd603cb1c96a1827479f8faea2df97/regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", size = 789759, upload-time = "2025-11-03T21:31:49.759Z" }, + { url = "https://files.pythonhosted.org/packages/59/9b/7c29be7903c318488983e7d97abcf8ebd3830e4c956c4c540005fcfb0462/regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", size = 266194, upload-time = "2025-11-03T21:31:51.53Z" }, + { url = "https://files.pythonhosted.org/packages/1a/67/3b92df89f179d7c367be654ab5626ae311cb28f7d5c237b6bb976cd5fbbb/regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", size = 277069, upload-time = "2025-11-03T21:31:53.151Z" }, + { url = "https://files.pythonhosted.org/packages/d7/55/85ba4c066fe5094d35b249c3ce8df0ba623cfd35afb22d6764f23a52a1c5/regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", size = 270330, upload-time = "2025-11-03T21:31:54.514Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081, upload-time = "2025-11-03T21:31:55.9Z" }, + { url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123, upload-time = "2025-11-03T21:31:57.758Z" }, + { url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814, upload-time = "2025-11-03T21:32:01.12Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592, upload-time = "2025-11-03T21:32:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122, upload-time = "2025-11-03T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272, upload-time = "2025-11-03T21:32:06.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497, upload-time = "2025-11-03T21:32:08.162Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892, upload-time = "2025-11-03T21:32:09.769Z" }, + { url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462, upload-time = "2025-11-03T21:32:11.769Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528, upload-time = "2025-11-03T21:32:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866, upload-time = "2025-11-03T21:32:15.748Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189, upload-time = "2025-11-03T21:32:17.493Z" }, + { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054, upload-time = "2025-11-03T21:32:19.042Z" }, + { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325, upload-time = "2025-11-03T21:32:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984, upload-time = "2025-11-03T21:32:23.466Z" }, + { url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673, upload-time = "2025-11-03T21:32:25.034Z" }, + { url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029, upload-time = "2025-11-03T21:32:26.528Z" }, + { url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437, upload-time = "2025-11-03T21:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368, upload-time = "2025-11-03T21:32:30.4Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921, upload-time = "2025-11-03T21:32:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708, upload-time = "2025-11-03T21:32:34.305Z" }, + { url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472, upload-time = "2025-11-03T21:32:36.364Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341, upload-time = "2025-11-03T21:32:38.042Z" }, + { url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666, upload-time = "2025-11-03T21:32:40.079Z" }, + { url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473, upload-time = "2025-11-03T21:32:42.148Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" }, + { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" }, + { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" }, + { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" }, + { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" }, + { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" }, + { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" }, + { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" }, + { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" }, + { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" }, + { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" }, +] + +[[package]] +name = "repo-to-text" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argparse" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/6f/83f8b76a88247ff90694a9741a416dfba25be65122037bfcf1886c78e648/repo_to_text-0.8.0.tar.gz", hash = "sha256:75555b6a7efaddd16737598bdc76bd26bad1cadde6b8a0cbf4fdc3060a3eb0ed", size = 163343, upload-time = "2025-10-25T13:36:10.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/b9/70a7c9e394f94ce8ef60ca35f67ba89dd8e83634fdc253b2ab5ad7ceeeab/repo_to_text-0.8.0-py3-none-any.whl", hash = "sha256:4252c53621696958a9b8f7f822cb2e79a8fa1aad1afb2f3bcd0fa05f3a4190bc", size = 14234, upload-time = "2025-10-25T13:36:08.999Z" }, +] + +[[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, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/dc/95f074d43452b3ef5d06276696ece4b3b5d696e7c9ad7173c54b1390cd70/rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea", size = 27419, upload-time = "2025-10-22T22:24:29.327Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/f8/13bb772dc7cbf2c3c5b816febc34fa0cb2c64a08e0569869585684ce6631/rpds_py-0.28.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7b6013db815417eeb56b2d9d7324e64fcd4fa289caeee6e7a78b2e11fc9b438a", size = 362820, upload-time = "2025-10-22T22:21:15.074Z" }, + { url = "https://files.pythonhosted.org/packages/84/91/6acce964aab32469c3dbe792cb041a752d64739c534e9c493c701ef0c032/rpds_py-0.28.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a4c6b05c685c0c03f80dabaeb73e74218c49deea965ca63f76a752807397207", size = 348499, upload-time = "2025-10-22T22:21:17.658Z" }, + { url = "https://files.pythonhosted.org/packages/f1/93/c05bb1f4f5e0234db7c4917cb8dd5e2e0a9a7b26dc74b1b7bee3c9cfd477/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4794c6c3fbe8f9ac87699b131a1f26e7b4abcf6d828da46a3a52648c7930eba", size = 379356, upload-time = "2025-10-22T22:21:19.847Z" }, + { url = "https://files.pythonhosted.org/packages/5c/37/e292da436f0773e319753c567263427cdf6c645d30b44f09463ff8216cda/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e8456b6ee5527112ff2354dd9087b030e3429e43a74f480d4a5ca79d269fd85", size = 390151, upload-time = "2025-10-22T22:21:21.569Z" }, + { url = "https://files.pythonhosted.org/packages/76/87/a4e3267131616e8faf10486dc00eaedf09bd61c87f01e5ef98e782ee06c9/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:beb880a9ca0a117415f241f66d56025c02037f7c4efc6fe59b5b8454f1eaa50d", size = 524831, upload-time = "2025-10-22T22:21:23.394Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c8/4a4ca76f0befae9515da3fad11038f0fce44f6bb60b21fe9d9364dd51fb0/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6897bebb118c44b38c9cb62a178e09f1593c949391b9a1a6fe777ccab5934ee7", size = 404687, upload-time = "2025-10-22T22:21:25.201Z" }, + { url = "https://files.pythonhosted.org/packages/6a/65/118afe854424456beafbbebc6b34dcf6d72eae3a08b4632bc4220f8240d9/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b553dd06e875249fd43efd727785efb57a53180e0fde321468222eabbeaafa", size = 382683, upload-time = "2025-10-22T22:21:26.536Z" }, + { url = "https://files.pythonhosted.org/packages/f7/bc/0625064041fb3a0c77ecc8878c0e8341b0ae27ad0f00cf8f2b57337a1e63/rpds_py-0.28.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:f0b2044fdddeea5b05df832e50d2a06fe61023acb44d76978e1b060206a8a476", size = 398927, upload-time = "2025-10-22T22:21:27.864Z" }, + { url = "https://files.pythonhosted.org/packages/5d/1a/fed7cf2f1ee8a5e4778f2054153f2cfcf517748875e2f5b21cf8907cd77d/rpds_py-0.28.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05cf1e74900e8da73fa08cc76c74a03345e5a3e37691d07cfe2092d7d8e27b04", size = 411590, upload-time = "2025-10-22T22:21:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/a8e0f67fa374a6c472dbb0afdaf1ef744724f165abb6899f20e2f1563137/rpds_py-0.28.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:efd489fec7c311dae25e94fe7eeda4b3d06be71c68f2cf2e8ef990ffcd2cd7e8", size = 559843, upload-time = "2025-10-22T22:21:30.917Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ea/e10353f6d7c105be09b8135b72787a65919971ae0330ad97d87e4e199880/rpds_py-0.28.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ada7754a10faacd4f26067e62de52d6af93b6d9542f0df73c57b9771eb3ba9c4", size = 584188, upload-time = "2025-10-22T22:21:32.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/b0/a19743e0763caf0c89f6fc6ba6fbd9a353b24ffb4256a492420c5517da5a/rpds_py-0.28.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c2a34fd26588949e1e7977cfcbb17a9a42c948c100cab890c6d8d823f0586457", size = 550052, upload-time = "2025-10-22T22:21:34.702Z" }, + { url = "https://files.pythonhosted.org/packages/de/bc/ec2c004f6c7d6ab1e25dae875cdb1aee087c3ebed5b73712ed3000e3851a/rpds_py-0.28.0-cp310-cp310-win32.whl", hash = "sha256:f9174471d6920cbc5e82a7822de8dfd4dcea86eb828b04fc8c6519a77b0ee51e", size = 215110, upload-time = "2025-10-22T22:21:36.645Z" }, + { url = "https://files.pythonhosted.org/packages/6c/de/4ce8abf59674e17187023933547d2018363e8fc76ada4f1d4d22871ccb6e/rpds_py-0.28.0-cp310-cp310-win_amd64.whl", hash = "sha256:6e32dd207e2c4f8475257a3540ab8a93eff997abfa0a3fdb287cae0d6cd874b8", size = 223850, upload-time = "2025-10-22T22:21:38.006Z" }, + { url = "https://files.pythonhosted.org/packages/a6/34/058d0db5471c6be7bef82487ad5021ff8d1d1d27794be8730aad938649cf/rpds_py-0.28.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:03065002fd2e287725d95fbc69688e0c6daf6c6314ba38bdbaa3895418e09296", size = 362344, upload-time = "2025-10-22T22:21:39.713Z" }, + { url = "https://files.pythonhosted.org/packages/5d/67/9503f0ec8c055a0782880f300c50a2b8e5e72eb1f94dfc2053da527444dd/rpds_py-0.28.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28ea02215f262b6d078daec0b45344c89e161eab9526b0d898221d96fdda5f27", size = 348440, upload-time = "2025-10-22T22:21:41.056Z" }, + { url = "https://files.pythonhosted.org/packages/68/2e/94223ee9b32332a41d75b6f94b37b4ce3e93878a556fc5f152cbd856a81f/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25dbade8fbf30bcc551cb352376c0ad64b067e4fc56f90e22ba70c3ce205988c", size = 379068, upload-time = "2025-10-22T22:21:42.593Z" }, + { url = "https://files.pythonhosted.org/packages/b4/25/54fd48f9f680cfc44e6a7f39a5fadf1d4a4a1fd0848076af4a43e79f998c/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c03002f54cc855860bfdc3442928ffdca9081e73b5b382ed0b9e8efe6e5e205", size = 390518, upload-time = "2025-10-22T22:21:43.998Z" }, + { url = "https://files.pythonhosted.org/packages/1b/85/ac258c9c27f2ccb1bd5d0697e53a82ebcf8088e3186d5d2bf8498ee7ed44/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9699fa7990368b22032baf2b2dce1f634388e4ffc03dfefaaac79f4695edc95", size = 525319, upload-time = "2025-10-22T22:21:45.645Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/c6734774789566d46775f193964b76627cd5f42ecf246d257ce84d1912ed/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9b06fe1a75e05e0713f06ea0c89ecb6452210fd60e2f1b6ddc1067b990e08d9", size = 404896, upload-time = "2025-10-22T22:21:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/14e37ce83202c632c89b0691185dca9532288ff9d390eacae3d2ff771bae/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9f83e7b326a3f9ec3ef84cda98fb0a74c7159f33e692032233046e7fd15da2", size = 382862, upload-time = "2025-10-22T22:21:49.176Z" }, + { url = "https://files.pythonhosted.org/packages/6a/83/f3642483ca971a54d60caa4449f9d6d4dbb56a53e0072d0deff51b38af74/rpds_py-0.28.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:0d3259ea9ad8743a75a43eb7819324cdab393263c91be86e2d1901ee65c314e0", size = 398848, upload-time = "2025-10-22T22:21:51.024Z" }, + { url = "https://files.pythonhosted.org/packages/44/09/2d9c8b2f88e399b4cfe86efdf2935feaf0394e4f14ab30c6c5945d60af7d/rpds_py-0.28.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a7548b345f66f6695943b4ef6afe33ccd3f1b638bd9afd0f730dd255c249c9e", size = 412030, upload-time = "2025-10-22T22:21:52.665Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f5/e1cec473d4bde6df1fd3738be8e82d64dd0600868e76e92dfeaebbc2d18f/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9a40040aa388b037eb39416710fbcce9443498d2eaab0b9b45ae988b53f5c67", size = 559700, upload-time = "2025-10-22T22:21:54.123Z" }, + { url = "https://files.pythonhosted.org/packages/8d/be/73bb241c1649edbf14e98e9e78899c2c5e52bbe47cb64811f44d2cc11808/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f60c7ea34e78c199acd0d3cda37a99be2c861dd2b8cf67399784f70c9f8e57d", size = 584581, upload-time = "2025-10-22T22:21:56.102Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9c/ffc6e9218cd1eb5c2c7dbd276c87cd10e8c2232c456b554169eb363381df/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1571ae4292649100d743b26d5f9c63503bb1fedf538a8f29a98dce2d5ba6b4e6", size = 549981, upload-time = "2025-10-22T22:21:58.253Z" }, + { url = "https://files.pythonhosted.org/packages/5f/50/da8b6d33803a94df0149345ee33e5d91ed4d25fc6517de6a25587eae4133/rpds_py-0.28.0-cp311-cp311-win32.whl", hash = "sha256:5cfa9af45e7c1140af7321fa0bef25b386ee9faa8928c80dc3a5360971a29e8c", size = 214729, upload-time = "2025-10-22T22:21:59.625Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/b0f48c4c320ee24c8c20df8b44acffb7353991ddf688af01eef5f93d7018/rpds_py-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd8d86b5d29d1b74100982424ba53e56033dc47720a6de9ba0259cf81d7cecaa", size = 223977, upload-time = "2025-10-22T22:22:01.092Z" }, + { url = "https://files.pythonhosted.org/packages/b4/21/c8e77a2ac66e2ec4e21f18a04b4e9a0417ecf8e61b5eaeaa9360a91713b4/rpds_py-0.28.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e27d3a5709cc2b3e013bf93679a849213c79ae0573f9b894b284b55e729e120", size = 217326, upload-time = "2025-10-22T22:22:02.944Z" }, + { url = "https://files.pythonhosted.org/packages/b8/5c/6c3936495003875fe7b14f90ea812841a08fca50ab26bd840e924097d9c8/rpds_py-0.28.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b4f28583a4f247ff60cd7bdda83db8c3f5b05a7a82ff20dd4b078571747708f", size = 366439, upload-time = "2025-10-22T22:22:04.525Z" }, + { url = "https://files.pythonhosted.org/packages/56/f9/a0f1ca194c50aa29895b442771f036a25b6c41a35e4f35b1a0ea713bedae/rpds_py-0.28.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d678e91b610c29c4b3d52a2c148b641df2b4676ffe47c59f6388d58b99cdc424", size = 348170, upload-time = "2025-10-22T22:22:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/18/ea/42d243d3a586beb72c77fa5def0487daf827210069a95f36328e869599ea/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e819e0e37a44a78e1383bf1970076e2ccc4dc8c2bbaa2f9bd1dc987e9afff628", size = 378838, upload-time = "2025-10-22T22:22:07.932Z" }, + { url = "https://files.pythonhosted.org/packages/e7/78/3de32e18a94791af8f33601402d9d4f39613136398658412a4e0b3047327/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5ee514e0f0523db5d3fb171f397c54875dbbd69760a414dccf9d4d7ad628b5bd", size = 393299, upload-time = "2025-10-22T22:22:09.435Z" }, + { url = "https://files.pythonhosted.org/packages/13/7e/4bdb435afb18acea2eb8a25ad56b956f28de7c59f8a1d32827effa0d4514/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3fa06d27fdcee47f07a39e02862da0100cb4982508f5ead53ec533cd5fe55e", size = 518000, upload-time = "2025-10-22T22:22:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/31/d0/5f52a656875cdc60498ab035a7a0ac8f399890cc1ee73ebd567bac4e39ae/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46959ef2e64f9e4a41fc89aa20dbca2b85531f9a72c21099a3360f35d10b0d5a", size = 408746, upload-time = "2025-10-22T22:22:13.143Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/49ce51767b879cde77e7ad9fae164ea15dce3616fe591d9ea1df51152706/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8455933b4bcd6e83fde3fefc987a023389c4b13f9a58c8d23e4b3f6d13f78c84", size = 386379, upload-time = "2025-10-22T22:22:14.602Z" }, + { url = "https://files.pythonhosted.org/packages/6a/99/e4e1e1ee93a98f72fc450e36c0e4d99c35370220e815288e3ecd2ec36a2a/rpds_py-0.28.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ad50614a02c8c2962feebe6012b52f9802deec4263946cddea37aaf28dd25a66", size = 401280, upload-time = "2025-10-22T22:22:16.063Z" }, + { url = "https://files.pythonhosted.org/packages/61/35/e0c6a57488392a8b319d2200d03dad2b29c0db9996f5662c3b02d0b86c02/rpds_py-0.28.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e5deca01b271492553fdb6c7fd974659dce736a15bae5dad7ab8b93555bceb28", size = 412365, upload-time = "2025-10-22T22:22:17.504Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6a/841337980ea253ec797eb084665436007a1aad0faac1ba097fb906c5f69c/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:735f8495a13159ce6a0d533f01e8674cec0c57038c920495f87dcb20b3ddb48a", size = 559573, upload-time = "2025-10-22T22:22:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/e7/5e/64826ec58afd4c489731f8b00729c5f6afdb86f1df1df60bfede55d650bb/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:961ca621ff10d198bbe6ba4957decca61aa2a0c56695384c1d6b79bf61436df5", size = 583973, upload-time = "2025-10-22T22:22:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ee/44d024b4843f8386a4eeaa4c171b3d31d55f7177c415545fd1a24c249b5d/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2374e16cc9131022e7d9a8f8d65d261d9ba55048c78f3b6e017971a4f5e6353c", size = 553800, upload-time = "2025-10-22T22:22:22.25Z" }, + { url = "https://files.pythonhosted.org/packages/7d/89/33e675dccff11a06d4d85dbb4d1865f878d5020cbb69b2c1e7b2d3f82562/rpds_py-0.28.0-cp312-cp312-win32.whl", hash = "sha256:d15431e334fba488b081d47f30f091e5d03c18527c325386091f31718952fe08", size = 216954, upload-time = "2025-10-22T22:22:24.105Z" }, + { url = "https://files.pythonhosted.org/packages/af/36/45f6ebb3210887e8ee6dbf1bc710ae8400bb417ce165aaf3024b8360d999/rpds_py-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:a410542d61fc54710f750d3764380b53bf09e8c4edbf2f9141a82aa774a04f7c", size = 227844, upload-time = "2025-10-22T22:22:25.551Z" }, + { url = "https://files.pythonhosted.org/packages/57/91/f3fb250d7e73de71080f9a221d19bd6a1c1eb0d12a1ea26513f6c1052ad6/rpds_py-0.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:1f0cfd1c69e2d14f8c892b893997fa9a60d890a0c8a603e88dca4955f26d1edd", size = 217624, upload-time = "2025-10-22T22:22:26.914Z" }, + { url = "https://files.pythonhosted.org/packages/d3/03/ce566d92611dfac0085c2f4b048cd53ed7c274a5c05974b882a908d540a2/rpds_py-0.28.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e9e184408a0297086f880556b6168fa927d677716f83d3472ea333b42171ee3b", size = 366235, upload-time = "2025-10-22T22:22:28.397Z" }, + { url = "https://files.pythonhosted.org/packages/00/34/1c61da1b25592b86fd285bd7bd8422f4c9d748a7373b46126f9ae792a004/rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edd267266a9b0448f33dc465a97cfc5d467594b600fe28e7fa2f36450e03053a", size = 348241, upload-time = "2025-10-22T22:22:30.171Z" }, + { url = "https://files.pythonhosted.org/packages/fc/00/ed1e28616848c61c493a067779633ebf4b569eccaacf9ccbdc0e7cba2b9d/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85beb8b3f45e4e32f6802fb6cd6b17f615ef6c6a52f265371fb916fae02814aa", size = 378079, upload-time = "2025-10-22T22:22:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/ccb30333a16a470091b6e50289adb4d3ec656fd9951ba8c5e3aaa0746a67/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2412be8d00a1b895f8ad827cc2116455196e20ed994bb704bf138fe91a42724", size = 393151, upload-time = "2025-10-22T22:22:33.453Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d0/73e2217c3ee486d555cb84920597480627d8c0240ff3062005c6cc47773e/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf128350d384b777da0e68796afdcebc2e9f63f0e9f242217754e647f6d32491", size = 517520, upload-time = "2025-10-22T22:22:34.949Z" }, + { url = "https://files.pythonhosted.org/packages/c4/91/23efe81c700427d0841a4ae7ea23e305654381831e6029499fe80be8a071/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2036d09b363aa36695d1cc1a97b36865597f4478470b0697b5ee9403f4fe399", size = 408699, upload-time = "2025-10-22T22:22:36.584Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ee/a324d3198da151820a326c1f988caaa4f37fc27955148a76fff7a2d787a9/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e1e9be4fa6305a16be628959188e4fd5cd6f1b0e724d63c6d8b2a8adf74ea6", size = 385720, upload-time = "2025-10-22T22:22:38.014Z" }, + { url = "https://files.pythonhosted.org/packages/19/ad/e68120dc05af8b7cab4a789fccd8cdcf0fe7e6581461038cc5c164cd97d2/rpds_py-0.28.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0a403460c9dd91a7f23fc3188de6d8977f1d9603a351d5db6cf20aaea95b538d", size = 401096, upload-time = "2025-10-22T22:22:39.869Z" }, + { url = "https://files.pythonhosted.org/packages/99/90/c1e070620042459d60df6356b666bb1f62198a89d68881816a7ed121595a/rpds_py-0.28.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d7366b6553cdc805abcc512b849a519167db8f5e5c3472010cd1228b224265cb", size = 411465, upload-time = "2025-10-22T22:22:41.395Z" }, + { url = "https://files.pythonhosted.org/packages/68/61/7c195b30d57f1b8d5970f600efee72a4fad79ec829057972e13a0370fd24/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b43c6a3726efd50f18d8120ec0551241c38785b68952d240c45ea553912ac41", size = 558832, upload-time = "2025-10-22T22:22:42.871Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3d/06f3a718864773f69941d4deccdf18e5e47dd298b4628062f004c10f3b34/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0cb7203c7bc69d7c1585ebb33a2e6074492d2fc21ad28a7b9d40457ac2a51ab7", size = 583230, upload-time = "2025-10-22T22:22:44.877Z" }, + { url = "https://files.pythonhosted.org/packages/66/df/62fc783781a121e77fee9a21ead0a926f1b652280a33f5956a5e7833ed30/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a52a5169c664dfb495882adc75c304ae1d50df552fbd68e100fdc719dee4ff9", size = 553268, upload-time = "2025-10-22T22:22:46.441Z" }, + { url = "https://files.pythonhosted.org/packages/84/85/d34366e335140a4837902d3dea89b51f087bd6a63c993ebdff59e93ee61d/rpds_py-0.28.0-cp313-cp313-win32.whl", hash = "sha256:2e42456917b6687215b3e606ab46aa6bca040c77af7df9a08a6dcfe8a4d10ca5", size = 217100, upload-time = "2025-10-22T22:22:48.342Z" }, + { url = "https://files.pythonhosted.org/packages/3c/1c/f25a3f3752ad7601476e3eff395fe075e0f7813fbb9862bd67c82440e880/rpds_py-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:e0a0311caedc8069d68fc2bf4c9019b58a2d5ce3cd7cb656c845f1615b577e1e", size = 227759, upload-time = "2025-10-22T22:22:50.219Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d6/5f39b42b99615b5bc2f36ab90423ea404830bdfee1c706820943e9a645eb/rpds_py-0.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:04c1b207ab8b581108801528d59ad80aa83bb170b35b0ddffb29c20e411acdc1", size = 217326, upload-time = "2025-10-22T22:22:51.647Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8b/0c69b72d1cee20a63db534be0df271effe715ef6c744fdf1ff23bb2b0b1c/rpds_py-0.28.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f296ea3054e11fc58ad42e850e8b75c62d9a93a9f981ad04b2e5ae7d2186ff9c", size = 355736, upload-time = "2025-10-22T22:22:53.211Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6d/0c2ee773cfb55c31a8514d2cece856dd299170a49babd50dcffb15ddc749/rpds_py-0.28.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5a7306c19b19005ad98468fcefeb7100b19c79fc23a5f24a12e06d91181193fa", size = 342677, upload-time = "2025-10-22T22:22:54.723Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1c/22513ab25a27ea205144414724743e305e8153e6abe81833b5e678650f5a/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d9b86aa501fed9862a443c5c3116f6ead8bc9296185f369277c42542bd646b", size = 371847, upload-time = "2025-10-22T22:22:56.295Z" }, + { url = "https://files.pythonhosted.org/packages/60/07/68e6ccdb4b05115ffe61d31afc94adef1833d3a72f76c9632d4d90d67954/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5bbc701eff140ba0e872691d573b3d5d30059ea26e5785acba9132d10c8c31d", size = 381800, upload-time = "2025-10-22T22:22:57.808Z" }, + { url = "https://files.pythonhosted.org/packages/73/bf/6d6d15df80781d7f9f368e7c1a00caf764436518c4877fb28b029c4624af/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5690671cd672a45aa8616d7374fdf334a1b9c04a0cac3c854b1136e92374fe", size = 518827, upload-time = "2025-10-22T22:22:59.826Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d3/2decbb2976cc452cbf12a2b0aaac5f1b9dc5dd9d1f7e2509a3ee00421249/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f1d92ecea4fa12f978a367c32a5375a1982834649cdb96539dcdc12e609ab1a", size = 399471, upload-time = "2025-10-22T22:23:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2c/f30892f9e54bd02e5faca3f6a26d6933c51055e67d54818af90abed9748e/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d252db6b1a78d0a3928b6190156042d54c93660ce4d98290d7b16b5296fb7cc", size = 377578, upload-time = "2025-10-22T22:23:03.52Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5d/3bce97e5534157318f29ac06bf2d279dae2674ec12f7cb9c12739cee64d8/rpds_py-0.28.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d61b355c3275acb825f8777d6c4505f42b5007e357af500939d4a35b19177259", size = 390482, upload-time = "2025-10-22T22:23:05.391Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f0/886bd515ed457b5bd93b166175edb80a0b21a210c10e993392127f1e3931/rpds_py-0.28.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:acbe5e8b1026c0c580d0321c8aae4b0a1e1676861d48d6e8c6586625055b606a", size = 402447, upload-time = "2025-10-22T22:23:06.93Z" }, + { url = "https://files.pythonhosted.org/packages/42/b5/71e8777ac55e6af1f4f1c05b47542a1eaa6c33c1cf0d300dca6a1c6e159a/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa23b6f0fc59b85b4c7d89ba2965af274346f738e8d9fc2455763602e62fd5f", size = 552385, upload-time = "2025-10-22T22:23:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cb/6ca2d70cbda5a8e36605e7788c4aa3bea7c17d71d213465a5a675079b98d/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7b14b0c680286958817c22d76fcbca4800ddacef6f678f3a7c79a1fe7067fe37", size = 575642, upload-time = "2025-10-22T22:23:10.348Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d4/407ad9960ca7856d7b25c96dcbe019270b5ffdd83a561787bc682c797086/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bcf1d210dfee61a6c86551d67ee1031899c0fdbae88b2d44a569995d43797712", size = 544507, upload-time = "2025-10-22T22:23:12.434Z" }, + { url = "https://files.pythonhosted.org/packages/51/31/2f46fe0efcac23fbf5797c6b6b7e1c76f7d60773e525cb65fcbc582ee0f2/rpds_py-0.28.0-cp313-cp313t-win32.whl", hash = "sha256:3aa4dc0fdab4a7029ac63959a3ccf4ed605fee048ba67ce89ca3168da34a1342", size = 205376, upload-time = "2025-10-22T22:23:13.979Z" }, + { url = "https://files.pythonhosted.org/packages/92/e4/15947bda33cbedfc134490a41841ab8870a72a867a03d4969d886f6594a2/rpds_py-0.28.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7b7d9d83c942855e4fdcfa75d4f96f6b9e272d42fffcb72cd4bb2577db2e2907", size = 215907, upload-time = "2025-10-22T22:23:15.5Z" }, + { url = "https://files.pythonhosted.org/packages/08/47/ffe8cd7a6a02833b10623bf765fbb57ce977e9a4318ca0e8cf97e9c3d2b3/rpds_py-0.28.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472", size = 353830, upload-time = "2025-10-22T22:23:17.03Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9f/890f36cbd83a58491d0d91ae0db1702639edb33fb48eeb356f80ecc6b000/rpds_py-0.28.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2", size = 341819, upload-time = "2025-10-22T22:23:18.57Z" }, + { url = "https://files.pythonhosted.org/packages/09/e3/921eb109f682aa24fb76207698fbbcf9418738f35a40c21652c29053f23d/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527", size = 373127, upload-time = "2025-10-22T22:23:20.216Z" }, + { url = "https://files.pythonhosted.org/packages/23/13/bce4384d9f8f4989f1a9599c71b7a2d877462e5fd7175e1f69b398f729f4/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733", size = 382767, upload-time = "2025-10-22T22:23:21.787Z" }, + { url = "https://files.pythonhosted.org/packages/23/e1/579512b2d89a77c64ccef5a0bc46a6ef7f72ae0cf03d4b26dcd52e57ee0a/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56", size = 517585, upload-time = "2025-10-22T22:23:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/62/3c/ca704b8d324a2591b0b0adcfcaadf9c862375b11f2f667ac03c61b4fd0a6/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8", size = 399828, upload-time = "2025-10-22T22:23:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/da/37/e84283b9e897e3adc46b4c88bb3f6ec92a43bd4d2f7ef5b13459963b2e9c/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370", size = 375509, upload-time = "2025-10-22T22:23:27.32Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c2/a980beab869d86258bf76ec42dec778ba98151f253a952b02fe36d72b29c/rpds_py-0.28.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d", size = 392014, upload-time = "2025-10-22T22:23:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/da/b5/b1d3c5f9d3fa5aeef74265f9c64de3c34a0d6d5cd3c81c8b17d5c8f10ed4/rpds_py-0.28.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728", size = 402410, upload-time = "2025-10-22T22:23:31.14Z" }, + { url = "https://files.pythonhosted.org/packages/74/ae/cab05ff08dfcc052afc73dcb38cbc765ffc86f94e966f3924cd17492293c/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01", size = 553593, upload-time = "2025-10-22T22:23:32.834Z" }, + { url = "https://files.pythonhosted.org/packages/70/80/50d5706ea2a9bfc9e9c5f401d91879e7c790c619969369800cde202da214/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515", size = 576925, upload-time = "2025-10-22T22:23:34.47Z" }, + { url = "https://files.pythonhosted.org/packages/ab/12/85a57d7a5855a3b188d024b099fd09c90db55d32a03626d0ed16352413ff/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e", size = 542444, upload-time = "2025-10-22T22:23:36.093Z" }, + { url = "https://files.pythonhosted.org/packages/6c/65/10643fb50179509150eb94d558e8837c57ca8b9adc04bd07b98e57b48f8c/rpds_py-0.28.0-cp314-cp314-win32.whl", hash = "sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f", size = 207968, upload-time = "2025-10-22T22:23:37.638Z" }, + { url = "https://files.pythonhosted.org/packages/b4/84/0c11fe4d9aaea784ff4652499e365963222481ac647bcd0251c88af646eb/rpds_py-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1", size = 218876, upload-time = "2025-10-22T22:23:39.179Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e0/3ab3b86ded7bb18478392dc3e835f7b754cd446f62f3fc96f4fe2aca78f6/rpds_py-0.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d", size = 212506, upload-time = "2025-10-22T22:23:40.755Z" }, + { url = "https://files.pythonhosted.org/packages/51/ec/d5681bb425226c3501eab50fc30e9d275de20c131869322c8a1729c7b61c/rpds_py-0.28.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b", size = 355433, upload-time = "2025-10-22T22:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/568c5e689e1cfb1ea8b875cffea3649260955f677fdd7ddc6176902d04cd/rpds_py-0.28.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a", size = 342601, upload-time = "2025-10-22T22:23:44.372Z" }, + { url = "https://files.pythonhosted.org/packages/32/fe/51ada84d1d2a1d9d8f2c902cfddd0133b4a5eb543196ab5161d1c07ed2ad/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592", size = 372039, upload-time = "2025-10-22T22:23:46.025Z" }, + { url = "https://files.pythonhosted.org/packages/07/c1/60144a2f2620abade1a78e0d91b298ac2d9b91bc08864493fa00451ef06e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba", size = 382407, upload-time = "2025-10-22T22:23:48.098Z" }, + { url = "https://files.pythonhosted.org/packages/45/ed/091a7bbdcf4038a60a461df50bc4c82a7ed6d5d5e27649aab61771c17585/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c", size = 518172, upload-time = "2025-10-22T22:23:50.16Z" }, + { url = "https://files.pythonhosted.org/packages/54/dd/02cc90c2fd9c2ef8016fd7813bfacd1c3a1325633ec8f244c47b449fc868/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91", size = 399020, upload-time = "2025-10-22T22:23:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/ab/81/5d98cc0329bbb911ccecd0b9e19fbf7f3a5de8094b4cda5e71013b2dd77e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed", size = 377451, upload-time = "2025-10-22T22:23:53.711Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/4d5bcd49e3dfed2d38e2dcb49ab6615f2ceb9f89f5a372c46dbdebb4e028/rpds_py-0.28.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b", size = 390355, upload-time = "2025-10-22T22:23:55.299Z" }, + { url = "https://files.pythonhosted.org/packages/3f/79/9f14ba9010fee74e4f40bf578735cfcbb91d2e642ffd1abe429bb0b96364/rpds_py-0.28.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e", size = 403146, upload-time = "2025-10-22T22:23:56.929Z" }, + { url = "https://files.pythonhosted.org/packages/39/4c/f08283a82ac141331a83a40652830edd3a4a92c34e07e2bbe00baaea2f5f/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1", size = 552656, upload-time = "2025-10-22T22:23:58.62Z" }, + { url = "https://files.pythonhosted.org/packages/61/47/d922fc0666f0dd8e40c33990d055f4cc6ecff6f502c2d01569dbed830f9b/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c", size = 576782, upload-time = "2025-10-22T22:24:00.312Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0c/5bafdd8ccf6aa9d3bfc630cfece457ff5b581af24f46a9f3590f790e3df2/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092", size = 544671, upload-time = "2025-10-22T22:24:02.297Z" }, + { url = "https://files.pythonhosted.org/packages/2c/37/dcc5d8397caa924988693519069d0beea077a866128719351a4ad95e82fc/rpds_py-0.28.0-cp314-cp314t-win32.whl", hash = "sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3", size = 205749, upload-time = "2025-10-22T22:24:03.848Z" }, + { url = "https://files.pythonhosted.org/packages/d7/69/64d43b21a10d72b45939a28961216baeb721cc2a430f5f7c3bfa21659a53/rpds_py-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578", size = 216233, upload-time = "2025-10-22T22:24:05.471Z" }, + { url = "https://files.pythonhosted.org/packages/ae/bc/b43f2ea505f28119bd551ae75f70be0c803d2dbcd37c1b3734909e40620b/rpds_py-0.28.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f5e7101145427087e493b9c9b959da68d357c28c562792300dd21a095118ed16", size = 363913, upload-time = "2025-10-22T22:24:07.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/f2/db318195d324c89a2c57dc5195058cbadd71b20d220685c5bd1da79ee7fe/rpds_py-0.28.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:31eb671150b9c62409a888850aaa8e6533635704fe2b78335f9aaf7ff81eec4d", size = 350452, upload-time = "2025-10-22T22:24:08.754Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/1391c819b8573a4898cedd6b6c5ec5bc370ce59e5d6bdcebe3c9c1db4588/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b55c1f64482f7d8bd39942f376bfdf2f6aec637ee8c805b5041e14eeb771db", size = 380957, upload-time = "2025-10-22T22:24:10.826Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5c/e5de68ee7eb7248fce93269833d1b329a196d736aefb1a7481d1e99d1222/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24743a7b372e9a76171f6b69c01aedf927e8ac3e16c474d9fe20d552a8cb45c7", size = 391919, upload-time = "2025-10-22T22:24:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/fb/4f/2376336112cbfeb122fd435d608ad8d5041b3aed176f85a3cb32c262eb80/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:389c29045ee8bbb1627ea190b4976a310a295559eaf9f1464a1a6f2bf84dde78", size = 528541, upload-time = "2025-10-22T22:24:14.197Z" }, + { url = "https://files.pythonhosted.org/packages/68/53/5ae232e795853dd20da7225c5dd13a09c0a905b1a655e92bdf8d78a99fd9/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23690b5827e643150cf7b49569679ec13fe9a610a15949ed48b85eb7f98f34ec", size = 405629, upload-time = "2025-10-22T22:24:16.001Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2d/351a3b852b683ca9b6b8b38ed9efb2347596973849ba6c3a0e99877c10aa/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f0c9266c26580e7243ad0d72fc3e01d6b33866cfab5084a6da7576bcf1c4f72", size = 384123, upload-time = "2025-10-22T22:24:17.585Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/870804daa00202728cc91cb8e2385fa9f1f4eb49857c49cfce89e304eae6/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4c6c4db5d73d179746951486df97fd25e92396be07fc29ee8ff9a8f5afbdfb27", size = 400923, upload-time = "2025-10-22T22:24:19.512Z" }, + { url = "https://files.pythonhosted.org/packages/53/25/3706b83c125fa2a0bccceac951de3f76631f6bd0ee4d02a0ed780712ef1b/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3b695a8fa799dd2cfdb4804b37096c5f6dba1ac7f48a7fbf6d0485bcd060316", size = 413767, upload-time = "2025-10-22T22:24:21.316Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f9/ce43dbe62767432273ed2584cef71fef8411bddfb64125d4c19128015018/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:6aa1bfce3f83baf00d9c5fcdbba93a3ab79958b4c7d7d1f55e7fe68c20e63912", size = 561530, upload-time = "2025-10-22T22:24:22.958Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/ffe77999ed8f81e30713dd38fd9ecaa161f28ec48bb80fa1cd9118399c27/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:7b0f9dceb221792b3ee6acb5438eb1f02b0cb2c247796a72b016dcc92c6de829", size = 585453, upload-time = "2025-10-22T22:24:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d2/4a73b18821fd4669762c855fd1f4e80ceb66fb72d71162d14da58444a763/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5d0145edba8abd3db0ab22b5300c99dc152f5c9021fab861be0f0544dc3cbc5f", size = 552199, upload-time = "2025-10-22T22:24:26.54Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/55/cccfca45157a2031dcbb5a462a67f7cf27f8b37d4b3b1cd7438f0f5c1df6/ruff-0.14.4.tar.gz", hash = "sha256:f459a49fe1085a749f15414ca76f61595f1a2cc8778ed7c279b6ca2e1fd19df3", size = 5587844, upload-time = "2025-11-06T22:07:45.033Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/b9/67240254166ae1eaa38dec32265e9153ac53645a6c6670ed36ad00722af8/ruff-0.14.4-py3-none-linux_armv6l.whl", hash = "sha256:e6604613ffbcf2297cd5dcba0e0ac9bd0c11dc026442dfbb614504e87c349518", size = 12606781, upload-time = "2025-11-06T22:07:01.841Z" }, + { url = "https://files.pythonhosted.org/packages/46/c8/09b3ab245d8652eafe5256ab59718641429f68681ee713ff06c5c549f156/ruff-0.14.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d99c0b52b6f0598acede45ee78288e5e9b4409d1ce7f661f0fa36d4cbeadf9a4", size = 12946765, upload-time = "2025-11-06T22:07:05.858Z" }, + { url = "https://files.pythonhosted.org/packages/14/bb/1564b000219144bf5eed2359edc94c3590dd49d510751dad26202c18a17d/ruff-0.14.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9358d490ec030f1b51d048a7fd6ead418ed0826daf6149e95e30aa67c168af33", size = 11928120, upload-time = "2025-11-06T22:07:08.023Z" }, + { url = "https://files.pythonhosted.org/packages/a3/92/d5f1770e9988cc0742fefaa351e840d9aef04ec24ae1be36f333f96d5704/ruff-0.14.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b40d27924f1f02dfa827b9c0712a13c0e4b108421665322218fc38caf615c2", size = 12370877, upload-time = "2025-11-06T22:07:10.015Z" }, + { url = "https://files.pythonhosted.org/packages/e2/29/e9282efa55f1973d109faf839a63235575519c8ad278cc87a182a366810e/ruff-0.14.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f5e649052a294fe00818650712083cddc6cc02744afaf37202c65df9ea52efa5", size = 12408538, upload-time = "2025-11-06T22:07:13.085Z" }, + { url = "https://files.pythonhosted.org/packages/8e/01/930ed6ecfce130144b32d77d8d69f5c610e6d23e6857927150adf5d7379a/ruff-0.14.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa082a8f878deeba955531f975881828fd6afd90dfa757c2b0808aadb437136e", size = 13141942, upload-time = "2025-11-06T22:07:15.386Z" }, + { url = "https://files.pythonhosted.org/packages/6a/46/a9c89b42b231a9f487233f17a89cbef9d5acd538d9488687a02ad288fa6b/ruff-0.14.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1043c6811c2419e39011890f14d0a30470f19d47d197c4858b2787dfa698f6c8", size = 14544306, upload-time = "2025-11-06T22:07:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/78/96/9c6cf86491f2a6d52758b830b89b78c2ae61e8ca66b86bf5a20af73d20e6/ruff-0.14.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a9f3a936ac27fb7c2a93e4f4b943a662775879ac579a433291a6f69428722649", size = 14210427, upload-time = "2025-11-06T22:07:19.832Z" }, + { url = "https://files.pythonhosted.org/packages/71/f4/0666fe7769a54f63e66404e8ff698de1dcde733e12e2fd1c9c6efb689cb5/ruff-0.14.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95643ffd209ce78bc113266b88fba3d39e0461f0cbc8b55fb92505030fb4a850", size = 13658488, upload-time = "2025-11-06T22:07:22.32Z" }, + { url = "https://files.pythonhosted.org/packages/ee/79/6ad4dda2cfd55e41ac9ed6d73ef9ab9475b1eef69f3a85957210c74ba12c/ruff-0.14.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:456daa2fa1021bc86ca857f43fe29d5d8b3f0e55e9f90c58c317c1dcc2afc7b5", size = 13354908, upload-time = "2025-11-06T22:07:24.347Z" }, + { url = "https://files.pythonhosted.org/packages/b5/60/f0b6990f740bb15c1588601d19d21bcc1bd5de4330a07222041678a8e04f/ruff-0.14.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f911bba769e4a9f51af6e70037bb72b70b45a16db5ce73e1f72aefe6f6d62132", size = 13587803, upload-time = "2025-11-06T22:07:26.327Z" }, + { url = "https://files.pythonhosted.org/packages/c9/da/eaaada586f80068728338e0ef7f29ab3e4a08a692f92eb901a4f06bbff24/ruff-0.14.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:76158a7369b3979fa878612c623a7e5430c18b2fd1c73b214945c2d06337db67", size = 12279654, upload-time = "2025-11-06T22:07:28.46Z" }, + { url = "https://files.pythonhosted.org/packages/66/d4/b1d0e82cf9bf8aed10a6d45be47b3f402730aa2c438164424783ac88c0ed/ruff-0.14.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f3b8f3b442d2b14c246e7aeca2e75915159e06a3540e2f4bed9f50d062d24469", size = 12357520, upload-time = "2025-11-06T22:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/04/f4/53e2b42cc82804617e5c7950b7079d79996c27e99c4652131c6a1100657f/ruff-0.14.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c62da9a06779deecf4d17ed04939ae8b31b517643b26370c3be1d26f3ef7dbde", size = 12719431, upload-time = "2025-11-06T22:07:33.831Z" }, + { url = "https://files.pythonhosted.org/packages/a2/94/80e3d74ed9a72d64e94a7b7706b1c1ebaa315ef2076fd33581f6a1cd2f95/ruff-0.14.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a443a83a1506c684e98acb8cb55abaf3ef725078be40237463dae4463366349", size = 13464394, upload-time = "2025-11-06T22:07:35.905Z" }, + { url = "https://files.pythonhosted.org/packages/54/1a/a49f071f04c42345c793d22f6cf5e0920095e286119ee53a64a3a3004825/ruff-0.14.4-py3-none-win32.whl", hash = "sha256:643b69cb63cd996f1fc7229da726d07ac307eae442dd8974dbc7cf22c1e18fff", size = 12493429, upload-time = "2025-11-06T22:07:38.43Z" }, + { url = "https://files.pythonhosted.org/packages/bc/22/e58c43e641145a2b670328fb98bc384e20679b5774258b1e540207580266/ruff-0.14.4-py3-none-win_amd64.whl", hash = "sha256:26673da283b96fe35fa0c939bf8411abec47111644aa9f7cfbd3c573fb125d2c", size = 13635380, upload-time = "2025-11-06T22:07:40.496Z" }, + { url = "https://files.pythonhosted.org/packages/30/bd/4168a751ddbbf43e86544b4de8b5c3b7be8d7167a2a5cb977d274e04f0a1/ruff-0.14.4-py3-none-win_arm64.whl", hash = "sha256:dd09c292479596b0e6fec8cd95c65c3a6dc68e9ad17b8f2382130f87ff6a75bb", size = 12663065, upload-time = "2025-11-06T22:07:42.603Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "scipy" +version = "1.16.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" }, + { url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" }, + { url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" }, + { url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" }, + { url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" }, + { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" }, + { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, + { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" }, + { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" }, + { url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856, upload-time = "2025-10-28T17:33:31.375Z" }, + { url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306, upload-time = "2025-10-28T17:33:36.516Z" }, + { url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371, upload-time = "2025-10-28T17:33:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877, upload-time = "2025-10-28T17:33:48.483Z" }, + { url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103, upload-time = "2025-10-28T17:33:56.495Z" }, + { url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297, upload-time = "2025-10-28T17:34:04.722Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756, upload-time = "2025-10-28T17:34:13.482Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566, upload-time = "2025-10-28T17:34:22.384Z" }, + { url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877, upload-time = "2025-10-28T17:35:51.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366, upload-time = "2025-10-28T17:35:59.014Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931, upload-time = "2025-10-28T17:34:31.451Z" }, + { url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081, upload-time = "2025-10-28T17:34:39.087Z" }, + { url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244, upload-time = "2025-10-28T17:34:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753, upload-time = "2025-10-28T17:34:51.793Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912, upload-time = "2025-10-28T17:34:59.8Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371, upload-time = "2025-10-28T17:35:08.173Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477, upload-time = "2025-10-28T17:35:16.7Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678, upload-time = "2025-10-28T17:35:26.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" }, + { url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469, upload-time = "2025-10-28T17:36:08.741Z" }, + { url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043, upload-time = "2025-10-28T17:36:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952, upload-time = "2025-10-28T17:36:22.966Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3a/3e8c01a4d742b730df368e063787c6808597ccb38636ed821d10b39ca51b/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512, upload-time = "2025-10-28T17:36:29.731Z" }, + { url = "https://files.pythonhosted.org/packages/1f/60/c45a12b98ad591536bfe5330cb3cfe1850d7570259303563b1721564d458/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639, upload-time = "2025-10-28T17:36:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729, upload-time = "2025-10-28T17:36:46.547Z" }, + { url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251, upload-time = "2025-10-28T17:36:55.161Z" }, + { url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681, upload-time = "2025-10-28T17:37:04.1Z" }, + { url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423, upload-time = "2025-10-28T17:38:20.005Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027, upload-time = "2025-10-28T17:38:24.966Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379, upload-time = "2025-10-28T17:37:14.061Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052, upload-time = "2025-10-28T17:37:21.709Z" }, + { url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183, upload-time = "2025-10-28T17:37:29.559Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d3/1b229e433074c5738a24277eca520a2319aac7465eea7310ea6ae0e98ae2/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174, upload-time = "2025-10-28T17:37:36.306Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/d9e148b0ec680c0f042581a2be79a28a7ab66c0c4946697f9e7553ead337/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852, upload-time = "2025-10-28T17:37:42.228Z" }, + { url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595, upload-time = "2025-10-28T17:37:48.102Z" }, + { url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269, upload-time = "2025-10-28T17:37:53.72Z" }, + { url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779, upload-time = "2025-10-28T17:37:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128, upload-time = "2025-10-28T17:38:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" }, +] + +[[package]] +name = "selenium" +version = "4.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "trio" }, + { name = "trio-websocket" }, + { name = "typing-extensions" }, + { name = "urllib3", extra = ["socks"] }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/e6/646d0a41fb9a64572043c3de80be2a4941f2aeb578f273cf3dae54fc9437/selenium-4.34.2.tar.gz", hash = "sha256:0f6d147595f08c6d4bad87b34c39dcacb4650aedc78e3956c8eac1bb752a3854", size = 896309, upload-time = "2025-07-08T12:54:54.785Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/2b/dee1c58bde0a747b2d75fa7282a190885a726fe95b18b8ce1dc52f9c0983/selenium-4.34.2-py3-none-any.whl", hash = "sha256:ea208f7db9e3b26e58c4a817ea9dd29454576d6ea55937d754df079ad588e1ad", size = 9410676, upload-time = "2025-07-08T12:54:48.725Z" }, +] + +[[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, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, +] + +[[package]] +name = "spdx-license-list" +version = "3.27.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/7f/d6928ac066dceff28e581edea85a9025100a233fc88e180f3890e872183d/spdx_license_list-3.27.0.tar.gz", hash = "sha256:a5e1f4e8d9bacc7c8829091068b07668194828a82a93420b448d61f2c872eddd", size = 17950, upload-time = "2025-07-08T02:50:01.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/d5/6fbc5770fc55e027dbd24571c4fd0b4ad6f2e310adbbda95ec39993f344c/spdx_license_list-3.27.0-py3-none-any.whl", hash = "sha256:60016acdd8eba5398b298541e3472152b75040e3e31465559d0ef19b08b76ce7", size = 17858, upload-time = "2025-07-08T02:50:00.094Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/3c/fa6517610dc641262b77cc7bf994ecd17465812c1b0585fe33e11be758ab/sse_starlette-3.0.3.tar.gz", hash = "sha256:88cfb08747e16200ea990c8ca876b03910a23b547ab3bd764c0d8eb81019b971", size = 21943, upload-time = "2025-10-30T18:44:20.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl", hash = "sha256:af5bf5a6f3933df1d9c7f8539633dc8444ca6a97ab2e2a7cd3b6e431ac03a431", size = 11765, upload-time = "2025-10-30T18:44:18.834Z" }, +] + +[[package]] +name = "starlette" +version = "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, upload-time = "2025-04-13T13:56:17.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, +] + +[[package]] +name = "temporalio" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nexus-rpc" }, + { name = "protobuf" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "types-protobuf" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/20/b52c96b37bf00ead6e8a4a197075770ebad516db765cc3abca8396de0689/temporalio-1.18.0.tar.gz", hash = "sha256:7ff7f833eb1e7697084b4ed9d86c3167cbff1ec77f1b40df774313a5d0fd5f6d", size = 1781572, upload-time = "2025-09-19T23:40:52.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/28/c5a4ee259748450ac0765837f8c78cbfa36800264158d98bd2cde4496d87/temporalio-1.18.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ac5d30d8b010c9b042065ea1259da7638db1a0a25e81ee4be0671a393ed329c5", size = 12734753, upload-time = "2025-09-19T23:40:06.575Z" }, + { url = "https://files.pythonhosted.org/packages/be/94/24bd903b5594420a4d131bfa3de965313f9a409af77b47e9a9a56d85bb9e/temporalio-1.18.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:19315d192247230c9bd7c60a566c2b3a80ad4d9de891c6aa13df63d72d3ec169", size = 12323141, upload-time = "2025-09-19T23:40:16.817Z" }, + { url = "https://files.pythonhosted.org/packages/6d/76/82415b43c68e2c6bb3a85e8800555d206767815088c8cad0ade9a06bd7ac/temporalio-1.18.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a023b25033e48b2e43f623a78737047a45b8cb553f69f457d09272fce5c723da", size = 12694061, upload-time = "2025-09-19T23:40:26.388Z" }, + { url = "https://files.pythonhosted.org/packages/41/60/176a3224c2739fee270052dd9224ae36370c4e13d2ab1bb96a2f9bbb513c/temporalio-1.18.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:695211dddbcffc20077d5b3b9a9b41bd09f60393c4ff211bcc7d6d895d607cc1", size = 12879404, upload-time = "2025-09-19T23:40:37.487Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8d/e3809b356262d1d398d8cbb78df1e19d460c0a89e6ab64ca8d9c05d5fe5a/temporalio-1.18.0-cp39-abi3-win_amd64.whl", hash = "sha256:e3f691bd0a01a22c0fe40e87b6236cc8a292628e3a5a490880d1bf94709249c9", size = 13088041, upload-time = "2025-09-19T23:40:49.469Z" }, +] + +[[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, upload-time = "2025-04-02T08:25:09.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, +] + +[[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, upload-time = "2025-02-14T06:03:01.003Z" } +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, upload-time = "2025-02-14T06:02:01.251Z" }, + { 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, upload-time = "2025-02-14T06:02:02.869Z" }, + { 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, upload-time = "2025-02-14T06:02:04.165Z" }, + { 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, upload-time = "2025-02-14T06:02:06.268Z" }, + { 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, upload-time = "2025-02-14T06:02:08.889Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4c/22eb8e9856a2b1808d0a002d171e534eac03f96dbe1161978d7389a59498/tiktoken-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:26113fec3bd7a352e4b33dbaf1bd8948de2507e30bd95a44e2b1156647bc01b4", size = 894026, upload-time = "2025-02-14T06:02:12.841Z" }, + { 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, upload-time = "2025-02-14T06:02:14.174Z" }, + { 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, upload-time = "2025-02-14T06:02:15.384Z" }, + { 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, upload-time = "2025-02-14T06:02:16.666Z" }, + { 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, upload-time = "2025-02-14T06:02:18.595Z" }, + { 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, upload-time = "2025-02-14T06:02:20.729Z" }, + { url = "https://files.pythonhosted.org/packages/6f/07/c67ad1724b8e14e2b4c8cca04b15da158733ac60136879131db05dda7c30/tiktoken-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:11a20e67fdf58b0e2dea7b8654a288e481bb4fc0289d3ad21291f8d0849915fb", size = 893895, upload-time = "2025-02-14T06:02:22.67Z" }, + { 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, upload-time = "2025-02-14T06:02:24.768Z" }, + { 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, upload-time = "2025-02-14T06:02:26.92Z" }, + { 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, upload-time = "2025-02-14T06:02:28.124Z" }, + { 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, upload-time = "2025-02-14T06:02:29.845Z" }, + { 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, upload-time = "2025-02-14T06:02:33.838Z" }, + { url = "https://files.pythonhosted.org/packages/5b/64/b16003419a1d7728d0d8c0d56a4c24325e7b10a21a9dd1fc0f7115c02f0a/tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16", size = 894897, upload-time = "2025-02-14T06:02:36.265Z" }, + { 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, upload-time = "2025-02-14T06:02:37.494Z" }, + { 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, upload-time = "2025-02-14T06:02:39.516Z" }, + { 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, upload-time = "2025-02-14T06:02:41.791Z" }, + { 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, upload-time = "2025-02-14T06:02:43Z" }, + { 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, upload-time = "2025-02-14T06:02:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/de/a8/8f499c179ec900783ffe133e9aab10044481679bb9aad78436d239eee716/tiktoken-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ea0edb6f83dc56d794723286215918c1cde03712cbbafa0348b33448faf5b95", size = 894669, upload-time = "2025-02-14T06:02:47.341Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/46/fb6854cec3278fbfa4a75b50232c77622bc517ac886156e6afbfa4d8fc6e/tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9", size = 363123, upload-time = "2025-09-19T09:49:23.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/33/f4b2d94ada7ab297328fc671fed209368ddb82f965ec2224eb1892674c3a/tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73", size = 3069318, upload-time = "2025-09-19T09:49:11.848Z" }, + { url = "https://files.pythonhosted.org/packages/1c/58/2aa8c874d02b974990e89ff95826a4852a8b2a273c7d1b4411cdd45a4565/tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc", size = 2926478, upload-time = "2025-09-19T09:49:09.759Z" }, + { url = "https://files.pythonhosted.org/packages/1e/3b/55e64befa1e7bfea963cf4b787b2cea1011362c4193f5477047532ce127e/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a", size = 3256994, upload-time = "2025-09-19T09:48:56.701Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/fbfecf42f67d9b7b80fde4aabb2b3110a97fac6585c9470b5bff103a80cb/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7", size = 3153141, upload-time = "2025-09-19T09:48:59.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/a9/b38f4e74e0817af8f8ef925507c63c6ae8171e3c4cb2d5d4624bf58fca69/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21", size = 3508049, upload-time = "2025-09-19T09:49:05.868Z" }, + { url = "https://files.pythonhosted.org/packages/d2/48/dd2b3dac46bb9134a88e35d72e1aa4869579eacc1a27238f1577270773ff/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214", size = 3710730, upload-time = "2025-09-19T09:49:01.832Z" }, + { url = "https://files.pythonhosted.org/packages/93/0e/ccabc8d16ae4ba84a55d41345207c1e2ea88784651a5a487547d80851398/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f", size = 3412560, upload-time = "2025-09-19T09:49:03.867Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c6/dc3a0db5a6766416c32c034286d7c2d406da1f498e4de04ab1b8959edd00/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4", size = 3250221, upload-time = "2025-09-19T09:49:07.664Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a6/2c8486eef79671601ff57b093889a345dd3d576713ef047776015dc66de7/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879", size = 9345569, upload-time = "2025-09-19T09:49:14.214Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/32ce667f14c35537f5f605fe9bea3e415ea1b0a646389d2295ec348d5657/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446", size = 9271599, upload-time = "2025-09-19T09:49:16.639Z" }, + { url = "https://files.pythonhosted.org/packages/51/7c/a5f7898a3f6baa3fc2685c705e04c98c1094c523051c805cdd9306b8f87e/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a", size = 9533862, upload-time = "2025-09-19T09:49:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/36/65/7e75caea90bc73c1dd8d40438adf1a7bc26af3b8d0a6705ea190462506e1/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390", size = 9681250, upload-time = "2025-09-19T09:49:21.501Z" }, + { url = "https://files.pythonhosted.org/packages/30/2c/959dddef581b46e6209da82df3b78471e96260e2bc463f89d23b1bf0e52a/tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82", size = 2472003, upload-time = "2025-09-19T09:49:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684, upload-time = "2025-09-19T09:49:24.953Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[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, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "trio" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "outcome" }, + { name = "sniffio" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/c1/68d582b4d3a1c1f8118e18042464bb12a7c1b75d64d75111b297687041e3/trio-0.30.0.tar.gz", hash = "sha256:0781c857c0c81f8f51e0089929a26b5bb63d57f927728a5586f7e36171f064df", size = 593776, upload-time = "2025-04-21T00:48:19.507Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/8e/3f6dfda475ecd940e786defe6df6c500734e686c9cd0a0f8ef6821e9b2f2/trio-0.30.0-py3-none-any.whl", hash = "sha256:3bf4f06b8decf8d3cf00af85f40a89824669e2d033bb32469d34840edcfc22a5", size = 499194, upload-time = "2025-04-21T00:48:17.167Z" }, +] + +[[package]] +name = "trio-websocket" +version = "0.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "outcome" }, + { name = "trio" }, + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/3c/8b4358e81f2f2cfe71b66a267f023a91db20a817b9425dd964873796980a/trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae", size = 33549, upload-time = "2025-02-25T05:16:58.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/19/eb640a397bba49ba49ef9dbe2e7e5c04202ba045b6ce2ec36e9cadc51e04/trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6", size = 21221, upload-time = "2025-02-25T05:16:57.545Z" }, +] + +[[package]] +name = "typer" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/45/bcbc581f87c8d8f2a56b513eb994d07ea4546322818d95dc6a3caf2c928b/typer-0.7.0.tar.gz", hash = "sha256:ff797846578a9f2a201b53442aedeb543319466870fbe1c701eab66dd7681165", size = 251871, upload-time = "2022-11-05T19:43:54.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/44/56c3f48d2bb83d76f5c970aef8e2c3ebd6a832f09e3621c5395371fe6999/typer-0.7.0-py3-none-any.whl", hash = "sha256:b5e704f4e48ec263de1c0b3a2387cd405a13767d2f907f44c1a08cbad96f606d", size = 38377, upload-time = "2022-11-05T19:43:53.402Z" }, +] + +[[package]] +name = "typer-slim" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/45/81b94a52caed434b94da65729c03ad0fb7665fab0f7db9ee54c94e541403/typer_slim-0.20.0.tar.gz", hash = "sha256:9fc6607b3c6c20f5c33ea9590cbeb17848667c51feee27d9e314a579ab07d1a3", size = 106561, upload-time = "2025-10-20T17:03:46.642Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/dd/5cbf31f402f1cc0ab087c94d4669cfa55bd1e818688b910631e131d74e75/typer_slim-0.20.0-py3-none-any.whl", hash = "sha256:f42a9b7571a12b97dddf364745d29f12221865acef7a2680065f9bb29c7dc89d", size = 47087, upload-time = "2025-10-20T17:03:44.546Z" }, +] + +[[package]] +name = "types-protobuf" +version = "6.32.1.20251105" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ab/0dce6a9841b5ebf3e37401879bb8cc20724ad9c770a7649bee997696cc75/types_protobuf-6.32.1.20251105.tar.gz", hash = "sha256:641002611ff87dd9fedc38a39a29cacb9907ae5ce61489b53e99ca2074bef764", size = 63846, upload-time = "2025-11-05T03:04:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/57/3a0d89b33b7485b7ffd99ec7cf53b0c5c89194c481f0bd673fd67e5f273f/types_protobuf-6.32.1.20251105-py3-none-any.whl", hash = "sha256:a15109d38f7cfefd2539ef86d3f93a6a41c7cad53924f8aa1a51eaddbb72a660", size = 77890, upload-time = "2025-11-05T03:04:42.067Z" }, +] + +[[package]] +name = "types-pytz" +version = "2025.2.0.20251108" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/ff/c047ddc68c803b46470a357454ef76f4acd8c1088f5cc4891cdd909bfcf6/types_pytz-2025.2.0.20251108.tar.gz", hash = "sha256:fca87917836ae843f07129567b74c1929f1870610681b4c92cb86a3df5817bdb", size = 10961, upload-time = "2025-11-08T02:55:57.001Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c1/56ef16bf5dcd255155cc736d276efa6ae0a5c26fd685e28f0412a4013c01/types_pytz-2025.2.0.20251108-py3-none-any.whl", hash = "sha256:0f1c9792cab4eb0e46c52f8845c8f77cf1e313cb3d68bf826aa867fe4717d91c", size = 10116, upload-time = "2025-11-08T02:55:56.194Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20250913" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" }, +] + +[[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, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "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, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[package.optional-dependencies] +socks = [ + { name = "pysocks" }, +] + +[[package]] +name = "uvicorn" +version = "0.34.3" +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/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvicorn-worker" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gunicorn" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/c0/b5df8c9a31b0516a47703a669902b362ca1e569fed4f3daa1d4299b28be0/uvicorn_worker-0.3.0.tar.gz", hash = "sha256:6baeab7b2162ea6b9612cbe149aa670a76090ad65a267ce8e27316ed13c7de7b", size = 9181, upload-time = "2024-12-26T12:13:07.591Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/1f/4e5f8770c2cf4faa2c3ed3c19f9d4485ac9db0a6b029a7866921709bdc6c/uvicorn_worker-0.3.0-py3-none-any.whl", hash = "sha256:ef0fe8aad27b0290a9e602a256b03f5a5da3a9e5f942414ca587b645ec77dd52", size = 5346, upload-time = "2024-12-26T12:13:06.026Z" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.35.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "wsproto" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425, upload-time = "2022-08-23T19:58:21.447Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226, upload-time = "2022-08-23T19:58:19.96Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517, upload-time = "2025-10-06T14:08:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495, upload-time = "2025-10-06T14:08:46.2Z" }, + { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400, upload-time = "2025-10-06T14:08:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545, upload-time = "2025-10-06T14:08:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598, upload-time = "2025-10-06T14:08:51.215Z" }, + { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893, upload-time = "2025-10-06T14:08:53.144Z" }, + { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240, upload-time = "2025-10-06T14:08:55.036Z" }, + { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965, upload-time = "2025-10-06T14:08:56.722Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026, upload-time = "2025-10-06T14:08:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637, upload-time = "2025-10-06T14:09:00.506Z" }, + { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082, upload-time = "2025-10-06T14:09:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811, upload-time = "2025-10-06T14:09:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223, upload-time = "2025-10-06T14:09:05.401Z" }, + { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118, upload-time = "2025-10-06T14:09:11.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852, upload-time = "2025-10-06T14:09:12.958Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012, upload-time = "2025-10-06T14:09:14.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]