add drug parser; add iha estimation rules
This commit is contained in:
parent
bf1d988d36
commit
80916f6c3e
10 changed files with 1271 additions and 15 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -10,3 +10,4 @@ wheels/
|
|||
.venv
|
||||
.env
|
||||
logs/
|
||||
.docs/
|
||||
|
|
@ -7,8 +7,12 @@ requires-python = ">=3.13"
|
|||
dependencies = [
|
||||
"fastapi[standard]>=0.116.1",
|
||||
"httpx>=0.28.1",
|
||||
"pandas>=2.3.1",
|
||||
"pydantic>=2.11.7",
|
||||
"pydantic-settings>=2.0.0",
|
||||
"beautifulsoup4>=4.12.0",
|
||||
"lxml>=4.9.0",
|
||||
"requests>=2.31.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
|
|
|||
|
|
@ -136,11 +136,30 @@ async def estimate(request: models.EstimationRequest):
|
|||
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(
|
||||
status=estimation_result.get("status", "accepted"),
|
||||
details=models.EstimationDetails(
|
||||
dtq=details.get("dtq", False),
|
||||
reason=details.get("reason", ""),
|
||||
reason=reason,
|
||||
tier=details.get("tier", 4),
|
||||
total_price=details.get("total_price", 0.0)
|
||||
),
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ class Settings(BaseSettings):
|
|||
TALESTORM_API_KEY: str
|
||||
TALESTORM_AGENT_ID: str
|
||||
TALESTORM_ESTIMATION_AGENT_ID: str
|
||||
VIRGIL_API_BASE_URL: str
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
116
src/drug_price_parser.py
Normal file
116
src/drug_price_parser.py
Normal 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")
|
||||
208
src/examples/estimation_example.py
Normal file
208
src/examples/estimation_example.py
Normal 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())
|
||||
304
src/examples/test_validation.py
Normal file
304
src/examples/test_validation.py
Normal 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())
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import httpx
|
||||
import json
|
||||
from typing import Dict, Any, List, Optional
|
||||
from ..config import settings
|
||||
from .session_service import session_service
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
from datetime import datetime, timedelta
|
||||
from src.config import settings
|
||||
from src.services.session_service import session_service
|
||||
|
||||
class EstimationService:
|
||||
"""Service for handling insurance estimation via TALESTORM API"""
|
||||
|
|
@ -11,6 +12,331 @@ class EstimationService:
|
|||
self.base_url = settings.TALESTORM_API_BASE_URL
|
||||
self.api_key = settings.TALESTORM_API_KEY
|
||||
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:
|
||||
"""Get HTTP client for TALESTORM API"""
|
||||
|
|
@ -26,8 +352,28 @@ class EstimationService:
|
|||
headers=headers
|
||||
)
|
||||
|
||||
def _format_estimation_request(self, request_data: Dict[str, Any]) -> str:
|
||||
"""Format the estimation request as a natural language message for the AI"""
|
||||
async def get_plan_details(self, plan_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""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", [])
|
||||
plans = request_data.get("plans", [])
|
||||
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" Nicotine use: {'Yes' if applicant.get('nicotine') else 'No'}")
|
||||
|
||||
# Add plan information
|
||||
# Fetch and add detailed plan information for all plans
|
||||
if plans:
|
||||
plan = plans[0]
|
||||
message_parts.append(f"\nPlan: Coverage type {plan.get('coverage', '')}")
|
||||
message_parts.append("\nPlan Details:")
|
||||
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
|
||||
if phq:
|
||||
|
|
@ -123,11 +489,28 @@ class EstimationService:
|
|||
async def estimate_insurance(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Send estimation request to TALESTORM API and parse response"""
|
||||
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
|
||||
session_id = await session_service.create_session(agent_id=self.agent_id)
|
||||
|
||||
# Format the request as a natural language message
|
||||
estimation_message = self._format_estimation_request(request_data)
|
||||
# Format the request as a natural language message with plan details
|
||||
estimation_message = await self._format_estimation_request_with_plan_details(request_data)
|
||||
|
||||
# Send request to TALESTORM API
|
||||
async with await self.get_client() as client:
|
||||
|
|
@ -194,6 +577,15 @@ class EstimationService:
|
|||
if field not in result:
|
||||
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
|
||||
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
|
|
@ -208,7 +600,8 @@ class EstimationService:
|
|||
"tier": 4,
|
||||
"total_price": 0.0
|
||||
},
|
||||
"results": []
|
||||
"results": [],
|
||||
"validation": validation_result
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -221,7 +614,8 @@ class EstimationService:
|
|||
"tier": 4,
|
||||
"total_price": 0.0
|
||||
},
|
||||
"results": []
|
||||
"results": [],
|
||||
"validation": validation_result if 'validation_result' in locals() else None
|
||||
}
|
||||
|
||||
# Global estimation service instance
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import httpx
|
||||
import uuid
|
||||
from typing import Optional, Dict, Any, List
|
||||
from ..config import settings
|
||||
from src.config import settings
|
||||
|
||||
class SessionService:
|
||||
"""Service for managing chat sessions with talestorm-ai"""
|
||||
|
|
|
|||
209
uv.lock
generated
209
uv.lock
generated
|
|
@ -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" },
|
||||
]
|
||||
|
||||
[[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.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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "click"
|
||||
version = "8.2.1"
|
||||
|
|
@ -254,10 +289,14 @@ name = "lolly-ai"
|
|||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "beautifulsoup4" },
|
||||
{ name = "fastapi", extra = ["standard"] },
|
||||
{ name = "httpx" },
|
||||
{ name = "lxml" },
|
||||
{ name = "pandas" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
|
@ -271,18 +310,46 @@ dev = [
|
|||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "beautifulsoup4", specifier = ">=4.12.0" },
|
||||
{ name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" },
|
||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.116.1" },
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ 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 = "pandas", specifier = ">=2.3.1" },
|
||||
{ name = "pydantic", specifier = ">=2.11.7" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.0.0" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" },
|
||||
{ name = "requests", specifier = ">=2.31.0" },
|
||||
]
|
||||
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]]
|
||||
name = "markdown-it-py"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "packaging"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "pathspec"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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 = "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" },
|
||||
]
|
||||
|
||||
[[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 = "pyyaml"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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.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" },
|
||||
]
|
||||
|
||||
[[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 = "sniffio"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "starlette"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "urllib3"
|
||||
version = "2.5.0"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue