add drug parser; add iha estimation rules

This commit is contained in:
ipu 2025-07-31 22:08:02 +03:00
parent bf1d988d36
commit 80916f6c3e
10 changed files with 1271 additions and 15 deletions

1
.gitignore vendored
View file

@ -10,3 +10,4 @@ wheels/
.venv .venv
.env .env
logs/ logs/
.docs/

View file

@ -7,8 +7,12 @@ requires-python = ">=3.13"
dependencies = [ dependencies = [
"fastapi[standard]>=0.116.1", "fastapi[standard]>=0.116.1",
"httpx>=0.28.1", "httpx>=0.28.1",
"pandas>=2.3.1",
"pydantic>=2.11.7", "pydantic>=2.11.7",
"pydantic-settings>=2.0.0", "pydantic-settings>=2.0.0",
"beautifulsoup4>=4.12.0",
"lxml>=4.9.0",
"requests>=2.31.0",
] ]
[project.optional-dependencies] [project.optional-dependencies]

View file

@ -136,11 +136,30 @@ async def estimate(request: models.EstimationRequest):
message=result.get("message", "") message=result.get("message", "")
)) ))
# Include validation information in the reason field if there are issues
reason = details.get("reason", "")
if "validation" in estimation_result:
validation_data = estimation_result["validation"]
if validation_data.get("issues"):
validation_issues = "; ".join(validation_data["issues"])
if reason:
reason = f"{reason}; Validation issues: {validation_issues}"
else:
reason = f"Validation issues: {validation_issues}"
# Add warnings to reason if any
if validation_data.get("warnings"):
validation_warnings = "; ".join(validation_data["warnings"])
if reason:
reason = f"{reason}; Warnings: {validation_warnings}"
else:
reason = f"Warnings: {validation_warnings}"
return models.EstimationResponse( return models.EstimationResponse(
status=estimation_result.get("status", "accepted"), status=estimation_result.get("status", "accepted"),
details=models.EstimationDetails( details=models.EstimationDetails(
dtq=details.get("dtq", False), dtq=details.get("dtq", False),
reason=details.get("reason", ""), reason=reason,
tier=details.get("tier", 4), tier=details.get("tier", 4),
total_price=details.get("total_price", 0.0) total_price=details.get("total_price", 0.0)
), ),

View file

@ -13,6 +13,7 @@ class Settings(BaseSettings):
TALESTORM_API_KEY: str TALESTORM_API_KEY: str
TALESTORM_AGENT_ID: str TALESTORM_AGENT_ID: str
TALESTORM_ESTIMATION_AGENT_ID: str TALESTORM_ESTIMATION_AGENT_ID: str
VIRGIL_API_BASE_URL: str

116
src/drug_price_parser.py Normal file
View file

@ -0,0 +1,116 @@
"""
Drug Price Parser for Drugs.com
This module provides functionality to scrape drug pricing information
from Drugs.com and return it in JSON format.
"""
import json
import re
import time
from typing import Dict, List, Optional, Any
from urllib.parse import quote_plus
import requests
from bs4 import BeautifulSoup
class DrugPriceParser:
"""Parser for extracting drug pricing information from Drugs.com"""
BASE_URL = "https://www.drugs.com/price-guide"
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate, br, zstd',
'Sec-GPC': '1',
'Connection': 'keep-alive',
'Cookie': 'ddc-pvc=8; ddcsubscribe=disabled',
'Upgrade-Insecure-Requests': '1',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1',
'Priority': 'u=0, i',
'TE': 'trailers'
})
def get_drug_prices(self, drug_name: str) -> Dict[str, Any]:
"""
Get pricing information for a specific drug.
Args:
drug_name: Name of the drug (e.g., 'alprazolam')
Returns:
Dictionary containing pricing information in JSON format
"""
url = f"{self.BASE_URL}/{drug_name.lower()}#prices"
user_agent = 'Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0'
self.session.headers.update({'User-Agent': user_agent})
response = self.session.get(url, timeout=15)
soup = BeautifulSoup(response.content, 'html.parser')
prices_data = self._extract_prices(soup, drug_name)
return {
"drug_name": drug_name,
"url": url,
"prices": prices_data,
"status": "success"
}
def _extract_prices(self, soup: BeautifulSoup, drug_name: str) -> Dict[str, Any]:
"""Extract pricing information from the parsed HTML"""
prices_data = {
"formulations": [],
}
div_content = soup.find('div', {'id': 'content'})
formulations = div_content.find_all('h3')
for formulation in formulations:
if formulation.get('class'):
break
formulation_name = formulation.get_text()
formulation_data = {
"name": formulation_name.rstrip(),
"dosages": []
}
dosages_table = formulation.find_next('div')
dosages = dosages_table.find_all('details')
for dosage in dosages:
summary = dosage.find('summary')
spans = summary.find_all('span')
dosage_name = spans[0].find('b').get_text()
dosage_price = spans[1].find_next('b').get_text()
formulation_data["dosages"].append({
"name": dosage_name.rstrip(),
"price": float(dosage_price.rstrip().replace('$', '').replace(',', ''))
})
prices_data["formulations"].append(formulation_data)
return prices_data
def parse_drug_prices(drug_name: str) -> str:
parser = DrugPriceParser()
result = parser.get_drug_prices(drug_name)
return json.dumps(result, indent=2)
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
drug_name = sys.argv[1]
result = parse_drug_prices(drug_name)
print(result)
else:
print("Usage: python drug_price_parser.py <drug_name>")
print("Example: python drug_price_parser.py alprazolam")

View file

@ -0,0 +1,208 @@
import asyncio
import json
from typing import Dict, Any
from services.estimation_service import estimation_service
async def example_estimation_request():
"""Example of how to use the estimation service"""
# Sample request data
request_data = {
"applicants": [
{
"firstName": "John",
"lastName": "Doe",
"dob": "1985-03-15",
"gender": "male",
"weight": 180,
"heightFt": 6,
"heightIn": 1,
"nicotine": False
},
{
"firstName": "Jane",
"lastName": "Doe",
"dob": "1988-07-22",
"gender": "female",
"weight": 140,
"heightFt": 5,
"heightIn": 6,
"nicotine": False
},
{
"firstName": "Emma",
"lastName": "Doe",
"dob": "2015-11-08",
"gender": "female",
"weight": 65,
"heightFt": 4,
"heightIn": 2,
"nicotine": False
}
],
"plans": [
{
"id": "plan_001",
"coverage": "family"
},
{
"id": "plan_002",
"coverage": "family"
}
],
"phq": {
"medications": [
{
"name": "Lisinopril",
"dosage": "10mg",
"frequency": "daily"
},
{
"name": "Metformin",
"dosage": "500mg",
"frequency": "twice daily"
},
{
"name": "Albuterol",
"dosage": "90mcg",
"frequency": "as needed"
}
],
"issues": [
{
"key": "diabetes",
"details": [
{
"description": "Type 2 diabetes diagnosed in 2020"
},
{
"description": "Well controlled with medication"
}
]
},
{
"key": "hypertension",
"details": [
{
"description": "High blood pressure managed with Lisinopril"
}
]
},
{
"key": "asthma",
"details": [
{
"description": "Mild asthma, uses inhaler as needed"
}
]
}
],
"conditions": [
{
"description": "Type 2 diabetes mellitus"
},
{
"description": "Essential hypertension"
},
{
"description": "Mild persistent asthma"
}
]
},
"income": 75000,
"address": {
"address1": "123 Main Street",
"address2": "Apt 4B",
"city": "Springfield",
"state": "IL",
"zipcode": "62701"
}
}
print("=== Insurance Estimation Service Example ===\n")
print("Request Data:")
print(json.dumps(request_data, indent=2))
print("\n" + "="*50 + "\n")
try:
# Call the estimation service
print("Calling estimation service...")
result = await estimation_service.estimate_insurance(request_data)
print("Estimation Result:")
print(json.dumps(result, indent=2))
# Display a summary
if result.get("status") == "accepted":
details = result.get("details", {})
print(f"\n=== SUMMARY ===")
print(f"Status: {result['status']}")
print(f"DTQ: {details.get('dtq', False)}")
print(f"Reason: {details.get('reason', 'N/A')}")
print(f"Tier: {details.get('tier', 'N/A')}")
print(f"Total Price: ${details.get('total_price', 0):,.2f}")
results = result.get("results", [])
if results:
print(f"\nApplicant Results:")
for applicant_result in results:
print(f"- {applicant_result.get('name', 'N/A')} ({applicant_result.get('applicant_type', 'N/A')})")
print(f" Age: {applicant_result.get('age', 'N/A')}")
print(f" BMI: {applicant_result.get('bmi', 'N/A')}")
print(f" Tier: {applicant_result.get('tier', 'N/A')}")
print(f" RX Spend: ${applicant_result.get('rx_spend', 0):,.2f}")
print(f" Message: {applicant_result.get('message', 'N/A')}")
else:
print(f"\nEstimation failed: {result.get('status', 'unknown')}")
if result.get("details"):
print(f"Reason: {result['details'].get('reason', 'N/A')}")
except Exception as e:
print(f"Error calling estimation service: {str(e)}")
async def example_minimal_request():
"""Example with minimal required data"""
minimal_request = {
"applicants": [
{
"firstName": "Alice",
"lastName": "Smith",
"dob": "1990-01-01",
"gender": "female",
"weight": 150,
"heightFt": 5,
"heightIn": 7,
"nicotine": False
}
],
"plans": [
{
"id": "basic_plan",
"coverage": "individual"
}
],
"income": 50000,
"address": {
"city": "Chicago",
"state": "IL"
}
}
print("\n=== Minimal Request Example ===\n")
print("Minimal Request Data:")
print(json.dumps(minimal_request, indent=2))
print("\n" + "="*50 + "\n")
try:
result = await estimation_service.estimate_insurance(minimal_request)
print("Minimal Request Result:")
print(json.dumps(result, indent=2))
except Exception as e:
print(f"Error with minimal request: {str(e)}")
if __name__ == "__main__":
# Run the examples
asyncio.run(example_estimation_request())
asyncio.run(example_minimal_request())

View file

@ -0,0 +1,304 @@
#!/usr/bin/env python3
"""
Test script for IHA underwriting validation functionality
"""
import asyncio
import sys
import os
# Add the src directory to the path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from services.estimation_service import estimation_service
async def test_validation():
"""Test various validation scenarios"""
print("Testing IHA Underwriting Validation\n")
# Test Case 1: Valid application
print("=== Test Case 1: Valid Application ===")
valid_request = {
"applicants": [
{
"firstName": "John",
"lastName": "Doe",
"dob": "15/03/1985",
"gender": "Male",
"weight": 180,
"heightFt": 6,
"heightIn": 0,
"nicotine": False
}
],
"plans": [{"id": "1", "coverage": 1, "tier": "standard"}],
"phq": {
"medications": [
{
"name": "Lisinopril",
"dosage": "10mg",
"frequency": "daily"
}
],
"conditions": [],
"issues": []
},
"income": 50000,
"address": {
"address1": "123 Main St",
"city": "Anytown",
"state": "CA",
"zipcode": "12345"
}
}
result1 = estimation_service._validate_application(valid_request)
print(f"Eligible: {result1['is_eligible']}")
print(f"Issues: {result1['issues']}")
print(f"Warnings: {result1['warnings']}")
print()
# Test Case 2: Uninsurable medication
print("=== Test Case 2: Uninsurable Medication ===")
uninsurable_med_request = {
"applicants": [
{
"firstName": "Jane",
"lastName": "Smith",
"dob": "20/07/1970",
"gender": "Female",
"weight": 150,
"heightFt": 5,
"heightIn": 6,
"nicotine": False
}
],
"plans": [{"id": "1", "coverage": 1, "tier": "standard"}],
"phq": {
"medications": [
{
"name": "Warfarin",
"dosage": "5mg",
"frequency": "daily"
}
],
"conditions": [],
"issues": []
},
"income": 60000,
"address": {
"address1": "456 Oak Ave",
"city": "Somewhere",
"state": "NY",
"zipcode": "67890"
}
}
result2 = estimation_service._validate_application(uninsurable_med_request)
print(f"Eligible: {result2['is_eligible']}")
print(f"Issues: {result2['issues']}")
print(f"Warnings: {result2['warnings']}")
print()
# Test Case 3: Uninsurable condition
print("=== Test Case 3: Uninsurable Condition ===")
uninsurable_condition_request = {
"applicants": [
{
"firstName": "Bob",
"lastName": "Johnson",
"dob": "10/12/1965",
"gender": "Male",
"weight": 200,
"heightFt": 5,
"heightIn": 10,
"nicotine": True
}
],
"plans": [{"id": "1", "coverage": 1, "tier": "standard"}],
"phq": {
"medications": [],
"conditions": [
{
"key": "cancer",
"description": "Lung cancer diagnosed 2 years ago"
}
],
"issues": []
},
"income": 40000,
"address": {
"address1": "789 Pine St",
"city": "Elsewhere",
"state": "TX",
"zipcode": "54321"
}
}
result3 = estimation_service._validate_application(uninsurable_condition_request)
print(f"Eligible: {result3['is_eligible']}")
print(f"Issues: {result3['issues']}")
print(f"Warnings: {result3['warnings']}")
print()
# Test Case 4: Height/weight issues
print("=== Test Case 4: Height/Weight Issues ===")
weight_issue_request = {
"applicants": [
{
"firstName": "Alice",
"lastName": "Brown",
"dob": "05/09/1980",
"gender": "Female",
"weight": 300, # Very high weight
"heightFt": 5,
"heightIn": 4,
"nicotine": False
}
],
"plans": [{"id": "1", "coverage": 1, "tier": "standard"}],
"phq": {
"medications": [],
"conditions": [],
"issues": []
},
"income": 70000,
"address": {
"address1": "321 Elm St",
"city": "Nowhere",
"state": "FL",
"zipcode": "98765"
}
}
result4 = estimation_service._validate_application(weight_issue_request)
print(f"Eligible: {result4['is_eligible']}")
print(f"Issues: {result4['issues']}")
print(f"Warnings: {result4['warnings']}")
print()
# Test Case 5: Diabetes risk factors
print("=== Test Case 5: Diabetes Risk Factors ===")
diabetes_risk_request = {
"applicants": [
{
"firstName": "Charlie",
"lastName": "Wilson",
"dob": "15/01/1975",
"gender": "Male",
"weight": 220,
"heightFt": 6,
"heightIn": 2,
"nicotine": False
}
],
"plans": [{"id": "1", "coverage": 1, "tier": "standard"}],
"phq": {
"medications": [
{
"name": "Insulin",
"dosage": "60 units",
"frequency": "daily"
},
{
"name": "Metformin",
"dosage": "1000mg",
"frequency": "twice daily"
},
{
"name": "Glipizide",
"dosage": "10mg",
"frequency": "daily"
},
{
"name": "Lisinopril",
"dosage": "20mg",
"frequency": "daily"
},
{
"name": "Amlodipine",
"dosage": "10mg",
"frequency": "daily"
},
{
"name": "Hydrochlorothiazide",
"dosage": "25mg",
"frequency": "daily"
}
],
"conditions": [],
"issues": []
},
"income": 55000,
"address": {
"address1": "654 Maple Dr",
"city": "Someplace",
"state": "OH",
"zipcode": "11111"
}
}
result5 = estimation_service._validate_application(diabetes_risk_request)
print(f"Eligible: {result5['is_eligible']}")
print(f"Issues: {result5['issues']}")
print(f"Warnings: {result5['warnings']}")
print()
# Test Case 6: Multiple applicants with mixed eligibility
print("=== Test Case 6: Multiple Applicants ===")
multiple_applicants_request = {
"applicants": [
{
"firstName": "David",
"lastName": "Miller",
"dob": "12/06/1988",
"gender": "Male",
"weight": 175,
"heightFt": 5,
"heightIn": 11,
"nicotine": False
},
{
"firstName": "Sarah",
"lastName": "Miller",
"dob": "08/11/1990",
"gender": "Female",
"weight": 140,
"heightFt": 5,
"heightIn": 6,
"nicotine": False
}
],
"plans": [{"id": "1", "coverage": 1, "tier": "standard"}],
"phq": {
"medications": [
{
"name": "Donepezil",
"dosage": "10mg",
"frequency": "daily"
}
],
"conditions": [],
"issues": []
},
"income": 80000,
"address": {
"address1": "987 Cedar Ln",
"city": "Anywhere",
"state": "WA",
"zipcode": "22222"
}
}
result6 = estimation_service._validate_application(multiple_applicants_request)
print(f"Eligible: {result6['is_eligible']}")
print(f"Issues: {result6['issues']}")
print(f"Warnings: {result6['warnings']}")
print("Applicant validations:")
for app_val in result6['applicant_validations']:
print(f" {app_val['name']}: BMI={app_val['bmi']:.1f}, Category={app_val['premium_category']}, Eligible={app_val['is_eligible']}")
print()
if __name__ == "__main__":
asyncio.run(test_validation())

View file

@ -1,8 +1,9 @@
import httpx import httpx
import json import json
from typing import Dict, Any, List, Optional from typing import Dict, Any, Optional, List, Tuple
from ..config import settings from datetime import datetime, timedelta
from .session_service import session_service from src.config import settings
from src.services.session_service import session_service
class EstimationService: class EstimationService:
"""Service for handling insurance estimation via TALESTORM API""" """Service for handling insurance estimation via TALESTORM API"""
@ -11,6 +12,331 @@ class EstimationService:
self.base_url = settings.TALESTORM_API_BASE_URL self.base_url = settings.TALESTORM_API_BASE_URL
self.api_key = settings.TALESTORM_API_KEY self.api_key = settings.TALESTORM_API_KEY
self.agent_id = settings.TALESTORM_ESTIMATION_AGENT_ID self.agent_id = settings.TALESTORM_ESTIMATION_AGENT_ID
self.insurance_api_base_url = settings.VIRGIL_API_BASE_URL
# Initialize uninsurable conditions and medications
self._initialize_underwriting_guidelines()
def _initialize_underwriting_guidelines(self):
"""Initialize uninsurable conditions and medications based on IHA guidelines"""
# Permanently uninsurable conditions
self.permanently_uninsurable_conditions = {
"organ_transplant", "amputation_disease", "emphysema", "chronic_pulmonary",
"oxygen_use", "nebulizer_use", "parkinsons", "multiple_sclerosis", "als",
"lupus", "myasthenia_gravis", "alzheimers", "dementia", "cognitive_disorders",
"aids", "arc", "hiv"
}
# Conditions uninsurable if diagnosed/treated in past 5 years
self.recent_uninsurable_conditions = {
"coronary_artery_disease", "heart_failure", "pacemaker", "defibrillator",
"enlarged_heart", "valve_surgery", "stroke", "tia", "carotid_disease",
"peripheral_vascular_disease", "rheumatoid_arthritis", "crippling_arthritis",
"cancer", "kidney_disease", "liver_disease", "hepatitis", "cirrhosis",
"alcoholism", "drug_abuse", "mental_nervous_hospitalization",
"heart_rhythm_disorders", "osteoporosis_fractures"
}
# Uninsurable medications (partial list)
self.uninsurable_medications = {
# HIV/AIDS medications
"abacavir", "lamivudine", "zidovudine", "tenofovir", "efavirenz", "nevirapine",
"atazanavir", "darunavir", "raltegravir", "dolutegravir",
# Cancer medications
"abarelix", "chlorambucil", "tamoxifen", "anastrozole", "letrozole",
"exemestane", "fulvestrant", "bicalutamide", "flutamide", "goserelin",
"leuprolide", "triptorelin", "bexarotene", "vorinostat", "romidepsin",
# Severe arthritis medications
"adalimumab", "methotrexate", "infliximab", "etanercept", "golimumab",
"certolizumab", "ustekinumab", "secukinumab", "ixekizumab", "guselkumab",
"tildrakizumab", "risankizumab", "upadacitinib", "tofacitinib", "baricitinib",
# Dementia medications
"donepezil", "memantine", "galantamine", "rivastigmine", "tacrine",
# Schizophrenia/psychosis medications
"aripiprazole", "olanzapine", "risperidone", "quetiapine", "ziprasidone",
"clozapine", "paliperidone", "asenapine", "iloperidone", "lurasidone",
"cariprazine", "brexpiprazole", "lumateperone",
# Parkinson's disease medications
"carbidopa", "levodopa", "selegiline", "rasagiline", "entacapone",
"tolcapone", "pramipexole", "ropinirole", "rotigotine", "apomorphine",
"amantadine", "trihexyphenidyl", "benztropine",
# Multiple sclerosis medications
"glatiramer", "interferon_beta", "natalizumab", "fingolimod", "teriflunomide",
"dimethyl_fumarate", "alemtuzumab", "ocrelizumab", "cladribine", "siponimod",
"ozanimod", "ponesimod",
# Severe cardiovascular disease medications
"amiodarone", "warfarin", "dabigatran", "rivaroxaban", "apixaban",
"edoxaban", "clopidogrel", "ticagrelor", "prasugrel", "dipyridamole",
"cilostazol", "pentoxifylline"
}
# Height and weight thresholds for premium categories
self.height_weight_chart = {
"5'0": {"preferred": (90, 102), "standard": (103, 218), "high": (219, 260)},
"5'1": {"preferred": (93, 105), "standard": (106, 221), "high": (222, 264)},
"5'2": {"preferred": (96, 108), "standard": (109, 224), "high": (225, 268)},
"5'3": {"preferred": (99, 111), "standard": (112, 227), "high": (228, 272)},
"5'4": {"preferred": (102, 114), "standard": (115, 230), "high": (231, 276)},
"5'5": {"preferred": (105, 117), "standard": (118, 233), "high": (234, 280)},
"5'6": {"preferred": (108, 120), "standard": (121, 236), "high": (237, 284)},
"5'7": {"preferred": (111, 123), "standard": (124, 239), "high": (240, 288)},
"5'8": {"preferred": (114, 126), "standard": (127, 242), "high": (243, 292)},
"5'9": {"preferred": (117, 129), "standard": (130, 245), "high": (246, 296)},
"5'10": {"preferred": (120, 132), "standard": (133, 248), "high": (249, 300)},
"5'11": {"preferred": (123, 135), "standard": (136, 251), "high": (252, 304)},
"6'0": {"preferred": (126, 138), "standard": (139, 254), "high": (255, 308)},
"6'1": {"preferred": (129, 141), "standard": (142, 257), "high": (258, 312)},
"6'2": {"preferred": (132, 144), "standard": (145, 260), "high": (261, 316)},
"6'3": {"preferred": (135, 147), "standard": (148, 263), "high": (264, 320)},
"6'4": {"preferred": (138, 150), "standard": (151, 266), "high": (267, 324)},
"6'5": {"preferred": (141, 153), "standard": (154, 269), "high": (270, 328)},
"6'6": {"preferred": (144, 156), "standard": (157, 272), "high": (273, 332)}
}
def _check_uninsurable_conditions(self, phq: Dict[str, Any]) -> List[str]:
"""Check for uninsurable conditions in PHQ data"""
issues = []
if not phq:
return issues
# Check conditions
for condition in phq.get("conditions", []):
condition_desc = condition.get("description", "").lower()
condition_key = condition.get("key", "").lower()
# Check for permanently uninsurable conditions
for uninsurable in self.permanently_uninsurable_conditions:
if uninsurable in condition_desc or uninsurable in condition_key:
issues.append(f"Permanently uninsurable condition: {condition.get('description', '')}")
# Check for recent uninsurable conditions (past 5 years)
for recent in self.recent_uninsurable_conditions:
if recent in condition_desc or recent in condition_key:
issues.append(f"Recent uninsurable condition (past 5 years): {condition.get('description', '')}")
# Check health issues
for issue in phq.get("issues", []):
issue_key = issue.get("key", "").lower()
issue_desc = " ".join([detail.get("description", "") for detail in issue.get("details", [])]).lower()
# Check for uninsurable issues
for uninsurable in self.permanently_uninsurable_conditions:
if uninsurable in issue_desc or uninsurable in issue_key:
issues.append(f"Permanently uninsurable health issue: {issue.get('key', '')}")
for recent in self.recent_uninsurable_conditions:
if recent in issue_desc or recent in issue_key:
issues.append(f"Recent uninsurable health issue (past 5 years): {issue.get('key', '')}")
return issues
def _check_uninsurable_medications(self, phq: Dict[str, Any]) -> List[str]:
"""Check for uninsurable medications in PHQ data"""
issues = []
if not phq:
return issues
for medication in phq.get("medications", []):
med_name = medication.get("name", "").lower()
# Check against uninsurable medication list
for uninsurable_med in self.uninsurable_medications:
if uninsurable_med in med_name:
issues.append(f"Uninsurable medication: {medication.get('name', '')}")
break
return issues
def _check_diabetes_risk_factors(self, phq: Dict[str, Any]) -> List[str]:
"""Check for diabetes-related uninsurability factors"""
issues = []
if not phq:
return issues
# Count diabetes and hypertension medications
diabetes_meds = []
hypertension_meds = []
for medication in phq.get("medications", []):
med_name = medication.get("name", "").lower()
# Diabetes medications
if any(dm_med in med_name for dm_med in ["insulin", "metformin", "glipizide", "glyburide", "glimepiride", "sitagliptin", "saxagliptin", "linagliptin", "alogliptin", "empagliflozin", "dapagliflozin", "canagliflozin", "dulaglutide", "liraglutide", "semaglutide", "exenatide"]):
diabetes_meds.append(medication.get("name", ""))
# Hypertension medications
if any(htn_med in med_name for htn_med in ["lisinopril", "enalapril", "ramipril", "quinapril", "benazepril", "fosinopril", "trandolapril", "moexipril", "perindopril", "losartan", "valsartan", "irbesartan", "candesartan", "olmesartan", "telmisartan", "eprosartan", "azilsartan", "amlodipine", "nifedipine", "felodipine", "isradipine", "nicardipine", "nifedipine", "diltiazem", "verapamil", "atenolol", "metoprolol", "propranolol", "carvedilol", "nebivolol", "bisoprolol", "hydrochlorothiazide", "chlorthalidone", "indapamide", "furosemide", "bumetanide", "torsemide", "spironolactone", "eplerenone", "doxazosin", "terazosin", "prazosin", "clonidine", "methyldopa", "hydralazine", "minoxidil"]):
hypertension_meds.append(medication.get("name", ""))
# Check for diabetes risk factors
if len(diabetes_meds) >= 3:
issues.append(f"High diabetes medication count ({len(diabetes_meds)}): {', '.join(diabetes_meds)}")
if len(hypertension_meds) >= 3:
issues.append(f"High hypertension medication count ({len(hypertension_meds)}): {', '.join(hypertension_meds)}")
# Check for insulin use > 50 units/day
for medication in phq.get("medications", []):
if "insulin" in medication.get("name", "").lower():
dosage = medication.get("dosage", "")
frequency = medication.get("frequency", "")
# Simple check for high insulin dosage (this would need more sophisticated parsing)
if "50" in dosage or "100" in dosage:
issues.append(f"High insulin dosage detected: {medication.get('name', '')} {dosage} {frequency}")
return issues
def _check_height_weight_eligibility(self, applicant: Dict[str, Any]) -> Tuple[str, str]:
"""Check height/weight eligibility and determine premium category"""
height_ft = applicant.get("heightFt", 0)
height_in = applicant.get("heightIn", 0)
weight = applicant.get("weight", 0)
if not all([height_ft, height_in, weight]):
return "unknown", "Missing height or weight information"
# Format height as "5'6" style
height_key = f"{height_ft}'{height_in}"
if height_key not in self.height_weight_chart:
return "unknown", f"Height {height_key} not in eligibility chart"
thresholds = self.height_weight_chart[height_key]
if weight < thresholds["preferred"][0]:
return "underweight", f"Weight {weight} lbs below minimum for height {height_key}"
elif weight <= thresholds["preferred"][1]:
return "preferred", "Eligible for preferred premium"
elif weight <= thresholds["standard"][1]:
return "standard", "Standard premium category"
elif weight <= thresholds["high"][1]:
return "high", "High premium category due to weight"
else:
return "uninsurable", f"Weight {weight} lbs exceeds maximum for height {height_key}"
return "unknown", "Unable to determine eligibility"
def _calculate_bmi(self, applicant: Dict[str, Any]) -> float:
"""Calculate BMI for an applicant"""
height_ft = applicant.get("heightFt", 0)
height_in = applicant.get("heightIn", 0)
weight = applicant.get("weight", 0)
if not all([height_ft, height_in, weight]):
return 0.0
# Convert to total inches
total_inches = (height_ft * 12) + height_in
# Convert to meters
height_m = total_inches * 0.0254
# Convert weight to kg
weight_kg = weight * 0.453592
# Calculate BMI
if height_m > 0:
return weight_kg / (height_m * height_m)
return 0.0
def _validate_application(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
"""Perform comprehensive IHA underwriting validation"""
validation_result = {
"is_eligible": True,
"issues": [],
"warnings": [],
"applicant_validations": []
}
applicants = request_data.get("applicants", [])
phq = request_data.get("phq", {})
# Check for uninsurable conditions
condition_issues = self._check_uninsurable_conditions(phq)
validation_result["issues"].extend(condition_issues)
# Check for uninsurable medications
medication_issues = self._check_uninsurable_medications(phq)
validation_result["issues"].extend(medication_issues)
# Check diabetes risk factors
diabetes_issues = self._check_diabetes_risk_factors(phq)
validation_result["issues"].extend(diabetes_issues)
# Validate each applicant
for i, applicant in enumerate(applicants):
applicant_validation = {
"applicant_index": i,
"name": f"{applicant.get('firstName', '')} {applicant.get('lastName', '')}",
"is_eligible": True,
"issues": [],
"warnings": [],
"bmi": 0.0,
"premium_category": "unknown"
}
# Calculate BMI
bmi = self._calculate_bmi(applicant)
applicant_validation["bmi"] = bmi
# Check height/weight eligibility
premium_category, message = self._check_height_weight_eligibility(applicant)
applicant_validation["premium_category"] = premium_category
if premium_category == "uninsurable":
applicant_validation["is_eligible"] = False
applicant_validation["issues"].append(f"Height/weight: {message}")
elif premium_category == "underweight":
applicant_validation["warnings"].append(f"Height/weight: {message}")
elif premium_category == "high":
applicant_validation["warnings"].append(f"Height/weight: {message}")
# Check for nicotine use
if applicant.get("nicotine"):
applicant_validation["warnings"].append("Nicotine use detected - may affect premium")
# Check age
try:
dob = applicant.get("dob", "")
if dob:
# Parse date of birth (assuming format like "dd/mm/yyyy")
if "/" in dob:
day, month, year = dob.split("/")
birth_date = datetime(int(year), int(month), int(day))
age = (datetime.now() - birth_date).days // 365
if age > 80:
applicant_validation["warnings"].append(f"Advanced age ({age} years) may affect eligibility")
elif age < 18:
applicant_validation["is_eligible"] = False
applicant_validation["issues"].append("Applicant must be 18 or older")
except:
applicant_validation["warnings"].append("Unable to determine age from date of birth")
validation_result["applicant_validations"].append(applicant_validation)
# If any applicant is ineligible, mark overall as ineligible
if not applicant_validation["is_eligible"]:
validation_result["is_eligible"] = False
# If there are any critical issues, mark as ineligible
if validation_result["issues"]:
validation_result["is_eligible"] = False
return validation_result
async def get_client(self) -> httpx.AsyncClient: async def get_client(self) -> httpx.AsyncClient:
"""Get HTTP client for TALESTORM API""" """Get HTTP client for TALESTORM API"""
@ -26,8 +352,28 @@ class EstimationService:
headers=headers headers=headers
) )
def _format_estimation_request(self, request_data: Dict[str, Any]) -> str: async def get_plan_details(self, plan_id: str) -> Optional[Dict[str, Any]]:
"""Format the estimation request as a natural language message for the AI""" """Fetch plan details from the insurance API"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.insurance_api_base_url}/insurance/plans/{plan_id}",
headers={"accept": "application/json"}
)
if response.status_code == 200:
return response.json()
else:
print(f"Failed to fetch plan {plan_id}: {response.status_code} {response.text}")
return None
except Exception as e:
print(f"Error fetching plan {plan_id}: {str(e)}")
return None
async def _format_estimation_request_with_plan_details(self, request_data: Dict[str, Any]) -> str:
"""Format the estimation request with fetched plan details"""
applicants = request_data.get("applicants", []) applicants = request_data.get("applicants", [])
plans = request_data.get("plans", []) plans = request_data.get("plans", [])
phq = request_data.get("phq", {}) phq = request_data.get("phq", {})
@ -48,10 +394,30 @@ class EstimationService:
message_parts.append(f" Height: {applicant.get('heightFt', '')}'{applicant.get('heightIn', '')}\"") message_parts.append(f" Height: {applicant.get('heightFt', '')}'{applicant.get('heightIn', '')}\"")
message_parts.append(f" Nicotine use: {'Yes' if applicant.get('nicotine') else 'No'}") message_parts.append(f" Nicotine use: {'Yes' if applicant.get('nicotine') else 'No'}")
# Add plan information # Fetch and add detailed plan information for all plans
if plans: if plans:
plan = plans[0] message_parts.append("\nPlan Details:")
message_parts.append(f"\nPlan: Coverage type {plan.get('coverage', '')}") for i, plan in enumerate(plans):
plan_id = plan.get('id')
if plan_id:
plan_details = await self.get_plan_details(plan_id)
if plan_details:
message_parts.append(f"\nPlan {i+1}:")
message_parts.append(f"- Plan Name: {plan_details.get('name', 'N/A')}")
message_parts.append(f"- Coverage Type: {plan_details.get('coverage_type', 'N/A')}")
message_parts.append(f"- Deductible: ${plan_details.get('deductible', 0):,.2f}")
message_parts.append(f"- Premium: ${plan_details.get('premium', 0):,.2f}")
message_parts.append(f"- Coinsurance: {plan_details.get('coinsurance', 0)}%")
message_parts.append(f"- Copay: ${plan_details.get('copay', 0):,.2f}")
if plan_details.get('benefits'):
message_parts.append("- Benefits:")
for benefit in plan_details['benefits']:
message_parts.append(f" * {benefit.get('name', '')}: {benefit.get('description', '')}")
else:
# Fallback to basic plan info if API call fails
message_parts.append(f"\nPlan {i+1}: Coverage type {plan.get('coverage', '')}")
else:
message_parts.append(f"\nPlan {i+1}: Coverage type {plan.get('coverage', '')}")
# Add PHQ information # Add PHQ information
if phq: if phq:
@ -123,11 +489,28 @@ class EstimationService:
async def estimate_insurance(self, request_data: Dict[str, Any]) -> Dict[str, Any]: async def estimate_insurance(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
"""Send estimation request to TALESTORM API and parse response""" """Send estimation request to TALESTORM API and parse response"""
try: try:
# Perform IHA underwriting validation first
validation_result = self._validate_application(request_data)
# If application is not eligible, return rejection immediately
if not validation_result["is_eligible"]:
return {
"status": "rejected",
"details": {
"dtq": False,
"reason": f"Application not eligible: {'; '.join(validation_result['issues'])}",
"tier": 4,
"total_price": 0.0
},
"results": [],
"validation": validation_result
}
# Create or get existing session with estimation agent # Create or get existing session with estimation agent
session_id = await session_service.create_session(agent_id=self.agent_id) session_id = await session_service.create_session(agent_id=self.agent_id)
# Format the request as a natural language message # Format the request as a natural language message with plan details
estimation_message = self._format_estimation_request(request_data) estimation_message = await self._format_estimation_request_with_plan_details(request_data)
# Send request to TALESTORM API # Send request to TALESTORM API
async with await self.get_client() as client: async with await self.get_client() as client:
@ -194,6 +577,15 @@ class EstimationService:
if field not in result: if field not in result:
raise ValueError(f"Missing required result field: {field}") raise ValueError(f"Missing required result field: {field}")
# Add validation information to the response
parsed_response["validation"] = validation_result
# If there are warnings, add them to the response
if validation_result["warnings"]:
if "warnings" not in parsed_response:
parsed_response["warnings"] = []
parsed_response["warnings"].extend(validation_result["warnings"])
return parsed_response return parsed_response
except (json.JSONDecodeError, ValueError) as e: except (json.JSONDecodeError, ValueError) as e:
@ -208,7 +600,8 @@ class EstimationService:
"tier": 4, "tier": 4,
"total_price": 0.0 "total_price": 0.0
}, },
"results": [] "results": [],
"validation": validation_result
} }
except Exception as e: except Exception as e:
@ -221,7 +614,8 @@ class EstimationService:
"tier": 4, "tier": 4,
"total_price": 0.0 "total_price": 0.0
}, },
"results": [] "results": [],
"validation": validation_result if 'validation_result' in locals() else None
} }
# Global estimation service instance # Global estimation service instance

View file

@ -1,7 +1,7 @@
import httpx import httpx
import uuid import uuid
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from ..config import settings from src.config import settings
class SessionService: class SessionService:
"""Service for managing chat sessions with talestorm-ai""" """Service for managing chat sessions with talestorm-ai"""

209
uv.lock generated
View file

@ -24,6 +24,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
] ]
[[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]] [[package]]
name = "black" name = "black"
version = "25.1.0" version = "25.1.0"
@ -53,6 +66,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" },
] ]
[[package]]
name = "charset-normalizer"
version = "3.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
]
[[package]] [[package]]
name = "click" name = "click"
version = "8.2.1" version = "8.2.1"
@ -254,10 +289,14 @@ name = "lolly-ai"
version = "0.1.0" version = "0.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "beautifulsoup4" },
{ name = "fastapi", extra = ["standard"] }, { name = "fastapi", extra = ["standard"] },
{ name = "httpx" }, { name = "httpx" },
{ name = "lxml" },
{ name = "pandas" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
{ name = "requests" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@ -271,18 +310,46 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "beautifulsoup4", specifier = ">=4.12.0" },
{ name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.116.1" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.116.1" },
{ name = "httpx", specifier = ">=0.28.1" }, { name = "httpx", specifier = ">=0.28.1" },
{ name = "isort", marker = "extra == 'dev'", specifier = ">=5.12.0" }, { name = "isort", marker = "extra == 'dev'", specifier = ">=5.12.0" },
{ name = "lxml", specifier = ">=4.9.0" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.7.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.7.0" },
{ name = "pandas", specifier = ">=2.3.1" },
{ name = "pydantic", specifier = ">=2.11.7" }, { name = "pydantic", specifier = ">=2.11.7" },
{ name = "pydantic-settings", specifier = ">=2.0.0" }, { name = "pydantic-settings", specifier = ">=2.0.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" },
{ name = "requests", specifier = ">=2.31.0" },
] ]
provides-extras = ["dev"] provides-extras = ["dev"]
[[package]]
name = "lxml"
version = "6.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload-time = "2025-06-26T16:28:19.373Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/21/6e7c060822a3c954ff085e5e1b94b4a25757c06529eac91e550f3f5cd8b8/lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da", size = 8414372, upload-time = "2025-06-26T16:26:39.079Z" },
{ url = "https://files.pythonhosted.org/packages/a4/f6/051b1607a459db670fc3a244fa4f06f101a8adf86cda263d1a56b3a4f9d5/lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7", size = 4593940, upload-time = "2025-06-26T16:26:41.891Z" },
{ url = "https://files.pythonhosted.org/packages/8e/74/dd595d92a40bda3c687d70d4487b2c7eff93fd63b568acd64fedd2ba00fe/lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3", size = 5214329, upload-time = "2025-06-26T16:26:44.669Z" },
{ url = "https://files.pythonhosted.org/packages/52/46/3572761efc1bd45fcafb44a63b3b0feeb5b3f0066886821e94b0254f9253/lxml-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d18a25b19ca7307045581b18b3ec9ead2b1db5ccd8719c291f0cd0a5cec6cb81", size = 4947559, upload-time = "2025-06-28T18:47:31.091Z" },
{ url = "https://files.pythonhosted.org/packages/94/8a/5e40de920e67c4f2eef9151097deb9b52d86c95762d8ee238134aff2125d/lxml-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4f0c66df4386b75d2ab1e20a489f30dc7fd9a06a896d64980541506086be1f1", size = 5102143, upload-time = "2025-06-28T18:47:33.612Z" },
{ url = "https://files.pythonhosted.org/packages/7c/4b/20555bdd75d57945bdabfbc45fdb1a36a1a0ff9eae4653e951b2b79c9209/lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24", size = 5021931, upload-time = "2025-06-26T16:26:47.503Z" },
{ url = "https://files.pythonhosted.org/packages/b6/6e/cf03b412f3763d4ca23b25e70c96a74cfece64cec3addf1c4ec639586b13/lxml-6.0.0-cp313-cp313-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a78d6c9168f5bcb20971bf3329c2b83078611fbe1f807baadc64afc70523b3a", size = 5645469, upload-time = "2025-07-03T19:19:13.32Z" },
{ url = "https://files.pythonhosted.org/packages/d4/dd/39c8507c16db6031f8c1ddf70ed95dbb0a6d466a40002a3522c128aba472/lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29", size = 5247467, upload-time = "2025-06-26T16:26:49.998Z" },
{ url = "https://files.pythonhosted.org/packages/4d/56/732d49def0631ad633844cfb2664563c830173a98d5efd9b172e89a4800d/lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4", size = 4720601, upload-time = "2025-06-26T16:26:52.564Z" },
{ url = "https://files.pythonhosted.org/packages/8f/7f/6b956fab95fa73462bca25d1ea7fc8274ddf68fb8e60b78d56c03b65278e/lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca", size = 5060227, upload-time = "2025-06-26T16:26:55.054Z" },
{ url = "https://files.pythonhosted.org/packages/97/06/e851ac2924447e8b15a294855caf3d543424364a143c001014d22c8ca94c/lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf", size = 4790637, upload-time = "2025-06-26T16:26:57.384Z" },
{ url = "https://files.pythonhosted.org/packages/06/d4/fd216f3cd6625022c25b336c7570d11f4a43adbaf0a56106d3d496f727a7/lxml-6.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4d6036c3a296707357efb375cfc24bb64cd955b9ec731abf11ebb1e40063949f", size = 5662049, upload-time = "2025-07-03T19:19:16.409Z" },
{ url = "https://files.pythonhosted.org/packages/52/03/0e764ce00b95e008d76b99d432f1807f3574fb2945b496a17807a1645dbd/lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef", size = 5272430, upload-time = "2025-06-26T16:27:00.031Z" },
{ url = "https://files.pythonhosted.org/packages/5f/01/d48cc141bc47bc1644d20fe97bbd5e8afb30415ec94f146f2f76d0d9d098/lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181", size = 3612896, upload-time = "2025-06-26T16:27:04.251Z" },
{ url = "https://files.pythonhosted.org/packages/f4/87/6456b9541d186ee7d4cb53bf1b9a0d7f3b1068532676940fdd594ac90865/lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e", size = 4013132, upload-time = "2025-06-26T16:27:06.415Z" },
{ url = "https://files.pythonhosted.org/packages/b7/42/85b3aa8f06ca0d24962f8100f001828e1f1f1a38c954c16e71154ed7d53a/lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03", size = 3672642, upload-time = "2025-06-26T16:27:09.888Z" },
]
[[package]] [[package]]
name = "markdown-it-py" name = "markdown-it-py"
version = "3.0.0" version = "3.0.0"
@ -361,6 +428,58 @@ 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" }, { 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 = "numpy"
version = "2.3.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1c/c0/c6bb172c916b00700ed3bf71cb56175fd1f7dbecebf8353545d0b5519f6c/numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3", size = 20949074, upload-time = "2025-07-24T20:43:07.813Z" },
{ url = "https://files.pythonhosted.org/packages/20/4e/c116466d22acaf4573e58421c956c6076dc526e24a6be0903219775d862e/numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b", size = 14177311, upload-time = "2025-07-24T20:43:29.335Z" },
{ url = "https://files.pythonhosted.org/packages/78/45/d4698c182895af189c463fc91d70805d455a227261d950e4e0f1310c2550/numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6", size = 5106022, upload-time = "2025-07-24T20:43:37.999Z" },
{ url = "https://files.pythonhosted.org/packages/9f/76/3e6880fef4420179309dba72a8c11f6166c431cf6dee54c577af8906f914/numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089", size = 6640135, upload-time = "2025-07-24T20:43:49.28Z" },
{ url = "https://files.pythonhosted.org/packages/34/fa/87ff7f25b3c4ce9085a62554460b7db686fef1e0207e8977795c7b7d7ba1/numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2", size = 14278147, upload-time = "2025-07-24T20:44:10.328Z" },
{ url = "https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f", size = 16635989, upload-time = "2025-07-24T20:44:34.88Z" },
{ url = "https://files.pythonhosted.org/packages/24/5a/84ae8dca9c9a4c592fe11340b36a86ffa9fd3e40513198daf8a97839345c/numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee", size = 16053052, upload-time = "2025-07-24T20:44:58.872Z" },
{ url = "https://files.pythonhosted.org/packages/57/7c/e5725d99a9133b9813fcf148d3f858df98511686e853169dbaf63aec6097/numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6", size = 18577955, upload-time = "2025-07-24T20:45:26.714Z" },
{ url = "https://files.pythonhosted.org/packages/ae/11/7c546fcf42145f29b71e4d6f429e96d8d68e5a7ba1830b2e68d7418f0bbd/numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b", size = 6311843, upload-time = "2025-07-24T20:49:24.444Z" },
{ url = "https://files.pythonhosted.org/packages/aa/6f/a428fd1cb7ed39b4280d057720fed5121b0d7754fd2a9768640160f5517b/numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56", size = 12782876, upload-time = "2025-07-24T20:49:43.227Z" },
{ url = "https://files.pythonhosted.org/packages/65/85/4ea455c9040a12595fb6c43f2c217257c7b52dd0ba332c6a6c1d28b289fe/numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2", size = 10192786, upload-time = "2025-07-24T20:49:59.443Z" },
{ url = "https://files.pythonhosted.org/packages/80/23/8278f40282d10c3f258ec3ff1b103d4994bcad78b0cba9208317f6bb73da/numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab", size = 21047395, upload-time = "2025-07-24T20:45:58.821Z" },
{ url = "https://files.pythonhosted.org/packages/1f/2d/624f2ce4a5df52628b4ccd16a4f9437b37c35f4f8a50d00e962aae6efd7a/numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2", size = 14300374, upload-time = "2025-07-24T20:46:20.207Z" },
{ url = "https://files.pythonhosted.org/packages/f6/62/ff1e512cdbb829b80a6bd08318a58698867bca0ca2499d101b4af063ee97/numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a", size = 5228864, upload-time = "2025-07-24T20:46:30.58Z" },
{ url = "https://files.pythonhosted.org/packages/7d/8e/74bc18078fff03192d4032cfa99d5a5ca937807136d6f5790ce07ca53515/numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286", size = 6737533, upload-time = "2025-07-24T20:46:46.111Z" },
{ url = "https://files.pythonhosted.org/packages/19/ea/0731efe2c9073ccca5698ef6a8c3667c4cf4eea53fcdcd0b50140aba03bc/numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8", size = 14352007, upload-time = "2025-07-24T20:47:07.1Z" },
{ url = "https://files.pythonhosted.org/packages/cf/90/36be0865f16dfed20f4bc7f75235b963d5939707d4b591f086777412ff7b/numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a", size = 16701914, upload-time = "2025-07-24T20:47:32.459Z" },
{ url = "https://files.pythonhosted.org/packages/94/30/06cd055e24cb6c38e5989a9e747042b4e723535758e6153f11afea88c01b/numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91", size = 16132708, upload-time = "2025-07-24T20:47:58.129Z" },
{ url = "https://files.pythonhosted.org/packages/9a/14/ecede608ea73e58267fd7cb78f42341b3b37ba576e778a1a06baffbe585c/numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5", size = 18651678, upload-time = "2025-07-24T20:48:25.402Z" },
{ url = "https://files.pythonhosted.org/packages/40/f3/2fe6066b8d07c3685509bc24d56386534c008b462a488b7f503ba82b8923/numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5", size = 6441832, upload-time = "2025-07-24T20:48:37.181Z" },
{ url = "https://files.pythonhosted.org/packages/0b/ba/0937d66d05204d8f28630c9c60bc3eda68824abde4cf756c4d6aad03b0c6/numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450", size = 12927049, upload-time = "2025-07-24T20:48:56.24Z" },
{ url = "https://files.pythonhosted.org/packages/e9/ed/13542dd59c104d5e654dfa2ac282c199ba64846a74c2c4bcdbc3a0f75df1/numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a", size = 10262935, upload-time = "2025-07-24T20:49:13.136Z" },
{ url = "https://files.pythonhosted.org/packages/c9/7c/7659048aaf498f7611b783e000c7268fcc4dcf0ce21cd10aad7b2e8f9591/numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a", size = 20950906, upload-time = "2025-07-24T20:50:30.346Z" },
{ url = "https://files.pythonhosted.org/packages/80/db/984bea9d4ddf7112a04cfdfb22b1050af5757864cfffe8e09e44b7f11a10/numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b", size = 14185607, upload-time = "2025-07-24T20:50:51.923Z" },
{ url = "https://files.pythonhosted.org/packages/e4/76/b3d6f414f4eca568f469ac112a3b510938d892bc5a6c190cb883af080b77/numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125", size = 5114110, upload-time = "2025-07-24T20:51:01.041Z" },
{ url = "https://files.pythonhosted.org/packages/9e/d2/6f5e6826abd6bca52392ed88fe44a4b52aacb60567ac3bc86c67834c3a56/numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19", size = 6642050, upload-time = "2025-07-24T20:51:11.64Z" },
{ url = "https://files.pythonhosted.org/packages/c4/43/f12b2ade99199e39c73ad182f103f9d9791f48d885c600c8e05927865baf/numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f", size = 14296292, upload-time = "2025-07-24T20:51:33.488Z" },
{ url = "https://files.pythonhosted.org/packages/5d/f9/77c07d94bf110a916b17210fac38680ed8734c236bfed9982fd8524a7b47/numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5", size = 16638913, upload-time = "2025-07-24T20:51:58.517Z" },
{ url = "https://files.pythonhosted.org/packages/9b/d1/9d9f2c8ea399cc05cfff8a7437453bd4e7d894373a93cdc46361bbb49a7d/numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58", size = 16071180, upload-time = "2025-07-24T20:52:22.827Z" },
{ url = "https://files.pythonhosted.org/packages/4c/41/82e2c68aff2a0c9bf315e47d61951099fed65d8cb2c8d9dc388cb87e947e/numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0", size = 18576809, upload-time = "2025-07-24T20:52:51.015Z" },
{ url = "https://files.pythonhosted.org/packages/14/14/4b4fd3efb0837ed252d0f583c5c35a75121038a8c4e065f2c259be06d2d8/numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2", size = 6366410, upload-time = "2025-07-24T20:56:44.949Z" },
{ url = "https://files.pythonhosted.org/packages/11/9e/b4c24a6b8467b61aced5c8dc7dcfce23621baa2e17f661edb2444a418040/numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b", size = 12918821, upload-time = "2025-07-24T20:57:06.479Z" },
{ url = "https://files.pythonhosted.org/packages/0e/0f/0dc44007c70b1007c1cef86b06986a3812dd7106d8f946c09cfa75782556/numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910", size = 10477303, upload-time = "2025-07-24T20:57:22.879Z" },
{ url = "https://files.pythonhosted.org/packages/8b/3e/075752b79140b78ddfc9c0a1634d234cfdbc6f9bbbfa6b7504e445ad7d19/numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e", size = 21047524, upload-time = "2025-07-24T20:53:22.086Z" },
{ url = "https://files.pythonhosted.org/packages/fe/6d/60e8247564a72426570d0e0ea1151b95ce5bd2f1597bb878a18d32aec855/numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45", size = 14300519, upload-time = "2025-07-24T20:53:44.053Z" },
{ url = "https://files.pythonhosted.org/packages/4d/73/d8326c442cd428d47a067070c3ac6cc3b651a6e53613a1668342a12d4479/numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b", size = 5228972, upload-time = "2025-07-24T20:53:53.81Z" },
{ url = "https://files.pythonhosted.org/packages/34/2e/e71b2d6dad075271e7079db776196829019b90ce3ece5c69639e4f6fdc44/numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2", size = 6737439, upload-time = "2025-07-24T20:54:04.742Z" },
{ url = "https://files.pythonhosted.org/packages/15/b0/d004bcd56c2c5e0500ffc65385eb6d569ffd3363cb5e593ae742749b2daa/numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0", size = 14352479, upload-time = "2025-07-24T20:54:25.819Z" },
{ url = "https://files.pythonhosted.org/packages/11/e3/285142fcff8721e0c99b51686426165059874c150ea9ab898e12a492e291/numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0", size = 16702805, upload-time = "2025-07-24T20:54:50.814Z" },
{ url = "https://files.pythonhosted.org/packages/33/c3/33b56b0e47e604af2c7cd065edca892d180f5899599b76830652875249a3/numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2", size = 16133830, upload-time = "2025-07-24T20:55:17.306Z" },
{ url = "https://files.pythonhosted.org/packages/6e/ae/7b1476a1f4d6a48bc669b8deb09939c56dd2a439db1ab03017844374fb67/numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf", size = 18652665, upload-time = "2025-07-24T20:55:46.665Z" },
{ url = "https://files.pythonhosted.org/packages/14/ba/5b5c9978c4bb161034148ade2de9db44ec316fab89ce8c400db0e0c81f86/numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1", size = 6514777, upload-time = "2025-07-24T20:55:57.66Z" },
{ url = "https://files.pythonhosted.org/packages/eb/46/3dbaf0ae7c17cdc46b9f662c56da2054887b8d9e737c1476f335c83d33db/numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b", size = 13111856, upload-time = "2025-07-24T20:56:17.318Z" },
{ url = "https://files.pythonhosted.org/packages/c1/9e/1652778bce745a67b5fe05adde60ed362d38eb17d919a540e813d30f6874/numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631", size = 10544226, upload-time = "2025-07-24T20:56:34.509Z" },
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "25.0" version = "25.0"
@ -370,6 +489,33 @@ 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" }, { 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 = "pandas"
version = "2.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "python-dateutil" },
{ name = "pytz" },
{ name = "tzdata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493, upload-time = "2025-07-07T19:20:04.079Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/ed/ff0a67a2c5505e1854e6715586ac6693dd860fbf52ef9f81edee200266e7/pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22", size = 11531393, upload-time = "2025-07-07T19:19:12.245Z" },
{ url = "https://files.pythonhosted.org/packages/c7/db/d8f24a7cc9fb0972adab0cc80b6817e8bef888cfd0024eeb5a21c0bb5c4a/pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a", size = 10668750, upload-time = "2025-07-07T19:19:14.612Z" },
{ url = "https://files.pythonhosted.org/packages/0f/b0/80f6ec783313f1e2356b28b4fd8d2148c378370045da918c73145e6aab50/pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928", size = 11342004, upload-time = "2025-07-07T19:19:16.857Z" },
{ url = "https://files.pythonhosted.org/packages/e9/e2/20a317688435470872885e7fc8f95109ae9683dec7c50be29b56911515a5/pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9", size = 12050869, upload-time = "2025-07-07T19:19:19.265Z" },
{ url = "https://files.pythonhosted.org/packages/55/79/20d746b0a96c67203a5bee5fb4e00ac49c3e8009a39e1f78de264ecc5729/pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12", size = 12750218, upload-time = "2025-07-07T19:19:21.547Z" },
{ url = "https://files.pythonhosted.org/packages/7c/0f/145c8b41e48dbf03dd18fdd7f24f8ba95b8254a97a3379048378f33e7838/pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb", size = 13416763, upload-time = "2025-07-07T19:19:23.939Z" },
{ url = "https://files.pythonhosted.org/packages/b2/c0/54415af59db5cdd86a3d3bf79863e8cc3fa9ed265f0745254061ac09d5f2/pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956", size = 10987482, upload-time = "2025-07-07T19:19:42.699Z" },
{ url = "https://files.pythonhosted.org/packages/48/64/2fd2e400073a1230e13b8cd604c9bc95d9e3b962e5d44088ead2e8f0cfec/pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a", size = 12029159, upload-time = "2025-07-07T19:19:26.362Z" },
{ url = "https://files.pythonhosted.org/packages/d8/0a/d84fd79b0293b7ef88c760d7dca69828d867c89b6d9bc52d6a27e4d87316/pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9", size = 11393287, upload-time = "2025-07-07T19:19:29.157Z" },
{ url = "https://files.pythonhosted.org/packages/50/ae/ff885d2b6e88f3c7520bb74ba319268b42f05d7e583b5dded9837da2723f/pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275", size = 11309381, upload-time = "2025-07-07T19:19:31.436Z" },
{ url = "https://files.pythonhosted.org/packages/85/86/1fa345fc17caf5d7780d2699985c03dbe186c68fee00b526813939062bb0/pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab", size = 11883998, upload-time = "2025-07-07T19:19:34.267Z" },
{ url = "https://files.pythonhosted.org/packages/81/aa/e58541a49b5e6310d89474333e994ee57fea97c8aaa8fc7f00b873059bbf/pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96", size = 12704705, upload-time = "2025-07-07T19:19:36.856Z" },
{ url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044, upload-time = "2025-07-07T19:19:39.999Z" },
]
[[package]] [[package]]
name = "pathspec" name = "pathspec"
version = "0.12.1" version = "0.12.1"
@ -496,6 +642,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157, upload-time = "2025-07-16T04:29:24.929Z" }, { url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157, upload-time = "2025-07-16T04:29:24.929Z" },
] ]
[[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]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.1.1" version = "1.1.1"
@ -514,6 +672,15 @@ 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" }, { 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 = "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]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.2" version = "6.0.2"
@ -531,6 +698,21 @@ wheels = [
{ 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" }, { 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 = "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]] [[package]]
name = "rich" name = "rich"
version = "14.0.0" version = "14.0.0"
@ -612,6 +794,15 @@ 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" }, { 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]] [[package]]
name = "sniffio" name = "sniffio"
version = "1.3.1" version = "1.3.1"
@ -621,6 +812,15 @@ 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" }, { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
] ]
[[package]]
name = "soupsieve"
version = "2.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" },
]
[[package]] [[package]]
name = "starlette" name = "starlette"
version = "0.47.2" version = "0.47.2"
@ -669,6 +869,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
] ]
[[package]]
name = "tzdata"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
]
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.5.0" version = "2.5.0"