diff --git a/.dockerignore b/.dockerignore index 1c1f7e9..dbbe012 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,25 +1,22 @@ # ============================================================================= # RFP Analyzer - Docker Ignore # ============================================================================= -# Files and directories to exclude from Docker build context # Git .git .gitignore -# Python +# .NET build artifacts +**/bin +**/obj +**/out +**/.vs + +# Python (legacy) __pycache__ *.py[cod] -*$py.class -*.so -.Python .venv venv -ENV -env -.eggs -*.egg-info -*.egg # IDE and editors .vscode @@ -39,9 +36,7 @@ Thumbs.db # Test files test.http -tests/ -*.test.py -*_test.py +**/tests/ # Documentation (not needed in container) docs/ @@ -49,19 +44,6 @@ docs/ !app/scoring_guide.md !README.md -# Build artifacts -dist/ -build/ -*.whl - # Logs *.log logs/ - -# uv cache (we use uv.lock but not cache) -.uv_cache - -# Misc -.coverage -.pytest_cache -htmlcov/ diff --git a/.gitignore b/.gitignore index d0fb0be..b0f6484 100644 --- a/.gitignore +++ b/.gitignore @@ -1,223 +1,429 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[codz] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Application logs -logs/ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates +*.env + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ + +[Dd]ebug/x64/ +[Dd]ebugPublic/x64/ +[Rr]elease/x64/ +[Rr]eleases/x64/ +bin/x64/ +obj/x64/ + +[Dd]ebug/x86/ +[Dd]ebugPublic/x86/ +[Rr]elease/x86/ +[Rr]eleases/x86/ +bin/x86/ +obj/x86/ + +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +[Aa][Rr][Mm]64[Ee][Cc]/ +bld/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Build results on 'Bin' directories +**/[Bb]in/* +# Uncomment if you have tasks that rely on *.refresh files to move binaries +# (https://github.com/github/gitignore/pull/3736) +#!**/[Bb]in/*.refresh + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* +*.trx + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Approval Tests result files +*.received.* + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.idb +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj *.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +**/.paket/paket.exe +paket-files/ + +# FAKE - F# Make +**/.fake/ + +# CodeRush personal settings +**/.cr/personal + +# Python Tools for Visual Studio (PTVS) +**/__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +#tools/** +#!tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py.cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -# Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -# poetry.lock -# poetry.toml - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. -# https://pdm-project.org/en/latest/usage/project/#working-with-version-control -# pdm.lock -# pdm.toml -.pdm-python -.pdm-build/ - -# pixi -# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. -# pixi.lock -# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one -# in the .venv directory. It is recommended not to include this directory in version control. -.pixi - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# Redis -*.rdb -*.aof -*.pid - -# RabbitMQ -mnesia/ -rabbitmq/ -rabbitmq-data/ - -# ActiveMQ -activemq-data/ - -# SageMath parsed files -*.sage.py - -# Environments -.env -.envrc -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -# .idea/ - -# Abstra -# Abstra is an AI-powered process automation framework. -# Ignore directories containing user credentials, local state, and settings. -# Learn more at https://abstra.io/docs -.abstra/ - -# Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore -# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, -# you could uncomment the following to ignore the entire vscode folder -# .vscode/ - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc - -# Marimo -marimo/_static/ -marimo/_lsp/ -__marimo__/ - -# Streamlit -.streamlit/secrets.toml -.azure +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog +MSBuild_Logs/ + +# AWS SAM Build and Temporary Artifacts folder +.aws-sam + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +**/.mfractor/ + +# Local History for Visual Studio +**/.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +**/.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp infra/demo2.bicep -test.http diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index f16e3e4..0000000 --- a/Dockerfile +++ /dev/null @@ -1,73 +0,0 @@ -# ============================================================================= -# RFP Analyzer - Dockerfile -# ============================================================================= -# Multi-stage build for optimized image size -# Includes WeasyPrint dependencies for PDF export - -# ----------------------------------------------------------------------------- -# Stage 1: Build stage with uv for fast dependency installation -# ----------------------------------------------------------------------------- -FROM python:3.13-slim AS builder - -# Install uv for fast package management -COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ - -# Set working directory -WORKDIR /app - -# Copy dependency files first for better caching (now from app folder) -COPY app/pyproject.toml app/uv.lock ./ - -# Install dependencies (including PDF export extras) -RUN uv sync --frozen --no-dev --all-extras - -# ----------------------------------------------------------------------------- -# Stage 2: Runtime stage -# ----------------------------------------------------------------------------- -FROM python:3.13-slim AS runtime - -# Install system dependencies for WeasyPrint (PDF export) -RUN apt-get update && apt-get install -y --no-install-recommends \ - libpango-1.0-0 \ - libpangocairo-1.0-0 \ - libgdk-pixbuf-2.0-0 \ - libffi-dev \ - shared-mime-info \ - && rm -rf /var/lib/apt/lists/* \ - && apt-get clean - -# Create non-root user for security -RUN useradd --create-home --shell /bin/bash appuser - -# Set working directory -WORKDIR /app - -# Copy virtual environment from builder -COPY --from=builder /app/.venv /app/.venv - -# Copy application code -COPY app/ ./ - -# Set environment variables -ENV PATH="/app/.venv/bin:$PATH" -ENV PYTHONPATH="/app" -ENV PYTHONUNBUFFERED=1 - -# Streamlit configuration -ENV STREAMLIT_SERVER_PORT=8501 -ENV STREAMLIT_SERVER_ADDRESS=0.0.0.0 -ENV STREAMLIT_SERVER_HEADLESS=true -ENV STREAMLIT_BROWSER_GATHER_USAGE_STATS=false - -# Switch to non-root user -USER appuser - -# Expose Streamlit port -EXPOSE 8501 - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8501/_stcore/health')" || exit 1 - -# Run Streamlit -CMD ["streamlit", "run", "main.py", "--server.port=8501", "--server.address=0.0.0.0"] diff --git a/README.md b/README.md index 4188896..bce5c17 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,21 @@ # RFP Analyzer [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/) +[![.NET 10](https://img.shields.io/badge/.NET-10-512BD4.svg)](https://dotnet.microsoft.com/) [![Azure](https://img.shields.io/badge/Azure-Powered-0078D4.svg)](https://azure.microsoft.com) -An AI-powered application for analyzing Request for Proposals (RFPs) and scoring vendor proposals using Azure AI services and a multi-agent architecture. +An AI-powered application for analyzing Request for Proposals (RFPs) and scoring vendor proposals using Azure AI services. Built with .NET 10 Blazor and MudBlazor. ## ๐ŸŽฏ Overview -RFP Analyzer automates the complex process of evaluating vendor proposals against RFP requirements. It leverages Azure AI services to extract document content, analyze evaluation criteria, and score proposals using a sophisticated multi-agent system. +RFP Analyzer automates the complex process of evaluating vendor proposals against RFP requirements. It leverages Azure AI services to extract document content, analyze evaluation criteria, and score proposals using specialized services. ### Key Capabilities - **Automated Document Processing**: Extract content from PDFs, Word documents, and images using Azure AI - **Intelligent Criteria Extraction**: Automatically identify evaluation criteria and weights from RFP documents - **Multi-Vendor Comparison**: Evaluate and rank multiple vendor proposals simultaneously -- **Comprehensive Reporting**: Generate detailed reports in Word, CSV, and JSON formats +- **Comprehensive Reporting**: Generate detailed reports in Excel, CSV, and JSON formats ## โœจ Features @@ -41,22 +41,19 @@ RFP Analyzer automates the complex process of evaluating vendor proposals agains | **Azure Content Understanding** | Complex documents, mixed content | Multi-modal analysis, layout understanding | | **Azure Document Intelligence** | Structured documents, forms | High accuracy OCR, pre-built models | -### Multi-Agent Architecture +### Scoring & Comparison Services -The evaluation system uses specialized AI agents: - -| Agent | Responsibility | -|-------|----------------| -| **Criteria Extraction Agent** | Analyzes RFP to identify scoring criteria, weights, and evaluation guidance | -| **Proposal Scoring Agent** | Evaluates each vendor proposal against extracted criteria | -| **Comparison Agent** | Compares vendors, generates rankings, and provides recommendations | +| Service | Responsibility | +|---------|----------------| +| **ScoringService** | Analyzes RFP to extract criteria and scores each vendor proposal | +| **ComparisonService** | Compares vendors, generates rankings, and provides recommendations | ### Export Options - ๐Ÿ“Š **CSV Reports**: Comparison matrices with all metrics -- ๐Ÿ“„ **Word Documents**: Detailed evaluation reports per vendor +- ๐Ÿ“— **Excel Documents**: Detailed evaluation reports per vendor - ๐Ÿ“‹ **JSON Data**: Structured data for further processing -- ๐Ÿ“ˆ **Interactive Charts**: Visual score comparisons (requires Plotly) +- ๐Ÿ“ˆ **Interactive Charts**: Visual score comparisons via Plotly.Blazor ## ๐Ÿ—๏ธ Architecture @@ -67,10 +64,10 @@ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for detailed diagrams and compo ```mermaid flowchart TB subgraph App["๐Ÿ–ฅ๏ธ RFP Analyzer Application"] - UI["Streamlit UI"] - DP["Document Processor"] - MAS["Multi-Agent Scoring System"] - UI --> DP --> MAS + UI["Blazor / MudBlazor UI"] + DP["Document Processor Service"] + SS["Scoring & Comparison Services"] + UI --> DP --> SS end subgraph Azure["โ˜๏ธ Azure AI Services"] @@ -90,7 +87,7 @@ flowchart TB | Resource | Purpose | |----------|---------| | **Azure AI Foundry Account** | Hosts AI services (OpenAI, Content Understanding, Document Intelligence) | -| **Azure Container Apps** | Runs the Streamlit application | +| **Azure Container Apps** | Runs the Blazor application | | **Azure Container Registry** | Stores application container images | | **Log Analytics Workspace** | Centralized logging and monitoring | | **Application Insights** | Application performance monitoring | @@ -100,8 +97,7 @@ flowchart TB ### Prerequisites -- **Python 3.13+** - [Download](https://www.python.org/downloads/) -- **UV Package Manager** - [Install UV](https://docs.astral.sh/uv/getting-started/installation/) +- **.NET 10 SDK** - [Download](https://dotnet.microsoft.com/download/dotnet/10.0) - **Azure CLI** - [Install Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli) - **Azure Developer CLI (azd)** - [Install azd](https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd) - **Docker** (optional) - For containerized deployment @@ -121,29 +117,24 @@ Your Azure subscription needs: cd rfp-analyzer ``` -2. **Install dependencies** - ```bash - cd app - uv sync - ``` - -3. **Configure environment** +2. **Authenticate with Azure** ```bash - cp .env.example .env - # Edit .env with your Azure credentials + az login ``` -4. **Authenticate with Azure** +3. **Configure app settings** ```bash - az login + # Edit app/RfpAnalyzer/appsettings.Development.json with your Azure endpoints ``` -5. **Run the application** +4. **Build and run** ```bash - uv run streamlit run main.py + cd app + dotnet build + dotnet run --project RfpAnalyzer ``` -6. **Open your browser** at `http://localhost:8501` +5. **Open your browser** at `https://localhost:5001` (or the URL shown in the console) ## โ˜๏ธ Azure Deployment @@ -204,46 +195,29 @@ The following environment variables are configured automatically during Azure de ### Manual Configuration (Local Development) -For local development, create a `.env` file in the `app` directory: - -```env -# Required -AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/ -AZURE_OPENAI_DEPLOYMENT_NAME=gpt-4o-mini - -# Choose one extraction service -AZURE_CONTENT_UNDERSTANDING_ENDPOINT=https://your-ai-foundry.services.ai.azure.com/ -# OR -AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT=https://your-doc-intel.cognitiveservices.azure.com/ - -# Optional: Enable OpenTelemetry logging -OTEL_LOGGING_ENABLED=false +For local development, edit `app/RfpAnalyzer/appsettings.Development.json`: + +```json +{ + "AzureOpenAI": { + "Endpoint": "https://your-resource.openai.azure.com/", + "DeploymentName": "gpt-4o-mini" + }, + "AzureContentUnderstanding": { + "Endpoint": "https://your-ai-foundry.services.ai.azure.com/" + }, + "AzureDocumentIntelligence": { + "Endpoint": "https://your-doc-intel.cognitiveservices.azure.com/" + } +} ``` ## ๐Ÿณ Docker Deployment -### Using Docker Compose (Recommended) - -```bash -cd app - -# Configure environment -cp .env.example .env -# Edit .env with your Azure credentials - -# Build and run -docker compose up --build - -# Run in background -docker compose up -d -``` - ### Using Docker Directly ```bash -cd app - -# Build the image +# Build the image from the repository root docker build -t rfp-analyzer . # Run the container @@ -266,25 +240,29 @@ rfp-analyzer/ โ”œโ”€โ”€ README.md # This file โ”œโ”€โ”€ LICENSE # MIT License โ”œโ”€โ”€ azure.yaml # Azure Developer CLI configuration -โ”œโ”€โ”€ Dockerfile # Root Dockerfile โ”œโ”€โ”€ docs/ โ”‚ โ””โ”€โ”€ ARCHITECTURE.md # Detailed architecture documentation โ”œโ”€โ”€ app/ -โ”‚ โ”œโ”€โ”€ main.py # Streamlit application entry point -โ”‚ โ”œโ”€โ”€ pyproject.toml # Python dependencies (UV) -โ”‚ โ”œโ”€โ”€ requirements.txt # Python dependencies (pip) -โ”‚ โ”œโ”€โ”€ Dockerfile # Application Dockerfile -โ”‚ โ”œโ”€โ”€ docker-compose.yml # Docker Compose configuration -โ”‚ โ”œโ”€โ”€ .env.example # Environment template -โ”‚ โ”œโ”€โ”€ scoring_guide.md # Default evaluation criteria -โ”‚ โ””โ”€โ”€ services/ -โ”‚ โ”œโ”€โ”€ document_processor.py # Document extraction orchestrator -โ”‚ โ”œโ”€โ”€ content_understanding_client.py # Azure Content Understanding -โ”‚ โ”œโ”€โ”€ document_intelligence_client.py # Azure Document Intelligence -โ”‚ โ”œโ”€โ”€ scoring_agent_v2.py # Multi-agent scoring system -โ”‚ โ”œโ”€โ”€ comparison_agent.py # Vendor comparison agent -โ”‚ โ”œโ”€โ”€ processing_queue.py # Async processing queue -โ”‚ โ””โ”€โ”€ logging_config.py # Centralized logging configuration +โ”‚ โ”œโ”€โ”€ Dockerfile # Multi-stage Docker build +โ”‚ โ”œโ”€โ”€ RfpAnalyzer.slnx # Solution file +โ”‚ โ”œโ”€โ”€ global.json # .NET SDK version pinning +โ”‚ โ”œโ”€โ”€ RfpAnalyzer/ # Main Blazor Web App project +โ”‚ โ”‚ โ”œโ”€โ”€ RfpAnalyzer.csproj # Project file (net10.0) +โ”‚ โ”‚ โ”œโ”€โ”€ Program.cs # Application entry point +โ”‚ โ”‚ โ”œโ”€โ”€ Components/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ App.razor # Root component +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Routes.razor # Routing configuration +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Layout/ # MainLayout, NavMenu +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ Pages/ # Upload, Extract, Evaluate pages +โ”‚ โ”‚ โ”œโ”€โ”€ Services/ +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ DocumentProcessorService.cs # Document extraction orchestrator +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ScoringService.cs # AI-powered proposal scoring +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ ComparisonService.cs # Vendor comparison & ranking +โ”‚ โ”‚ โ”œโ”€โ”€ Models/ # Data models (Scoring, Evaluation, etc.) +โ”‚ โ”‚ โ”œโ”€โ”€ Properties/ # Launch settings +โ”‚ โ”‚ โ””โ”€โ”€ wwwroot/ # Static assets +โ”‚ โ””โ”€โ”€ tests/ +โ”‚ โ””โ”€โ”€ RfpAnalyzer.Tests/ # Unit tests โ””โ”€โ”€ infra/ โ”œโ”€โ”€ main.bicep # Main infrastructure template โ”œโ”€โ”€ main.parameters.json # Deployment parameters @@ -299,13 +277,9 @@ rfp-analyzer/ ## ๐Ÿ”ง Configuration -### Scoring Guide - -Edit `app/scoring_guide.md` to customize the default evaluation criteria and weights. The scoring agent will use this as a reference when extracting criteria from RFPs that don't explicitly define evaluation metrics. - ### Document Processing -Choose between extraction services in the application sidebar: +Choose between extraction services in the application: - **Azure Content Understanding**: Best for complex documents with mixed content - **Azure Document Intelligence**: Best for structured documents and forms @@ -347,33 +321,30 @@ The application supports multiple Azure OpenAI models: Download results in your preferred format: - **CSV**: For spreadsheet analysis -- **Word**: For formal reporting +- **Excel**: For formal reporting - **JSON**: For integration with other systems ## ๐Ÿงช Development -### Running Tests +### Building ```bash cd app -uv run pytest +dotnet build ``` -### Code Quality +### Running Tests ```bash -# Format code -uv run ruff format . - -# Lint code -uv run ruff check . +cd app +dotnet test ``` ### Local Development with Hot Reload ```bash cd app -uv run streamlit run main.py --server.runOnSave true +dotnet watch --project RfpAnalyzer ``` ## ๐Ÿ“ฆ Dependencies @@ -382,20 +353,12 @@ uv run streamlit run main.py --server.runOnSave true | Package | Purpose | |---------|---------| -| `streamlit` | Web application framework | -| `agent-framework` | Microsoft Agent Framework for multi-agent orchestration | -| `azure-identity` | Azure authentication | -| `azure-ai-documentintelligence` | Document Intelligence SDK | -| `pydantic` | Data validation and models | -| `python-docx` | Word document generation | -| `plotly` | Interactive charts | - -### Optional Dependencies - -| Package | Purpose | Install | -|---------|---------|---------| -| `weasyprint` | PDF generation | `uv sync --extra pdf` | -| `markdown` | Markdown to HTML conversion | `uv sync --extra pdf` | +| `MudBlazor` | Material Design component library for Blazor | +| `Azure.Identity` | Azure authentication (DefaultAzureCredential) | +| `Azure.Core` | Azure SDK core library | +| `Plotly.Blazor` | Interactive charts | +| `ClosedXML` | Excel document generation | +| `CsvHelper` | CSV report generation | ## ๐Ÿ”’ Security @@ -421,8 +384,8 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## ๐Ÿ™ Acknowledgments - [Azure AI Services](https://azure.microsoft.com/products/ai-services/) for powerful AI capabilities -- [Streamlit](https://streamlit.io/) for the intuitive web framework -- [Microsoft Agent Framework](https://github.com/microsoft/agent-framework) for multi-agent orchestration +- [MudBlazor](https://mudblazor.com/) for the Material Design component library +- [.NET Blazor](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor) for the interactive web framework ## ๐Ÿ“ž Support @@ -432,4 +395,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file --- -**Built with โค๏ธ using Azure AI Services** +**Built with โค๏ธ using .NET 10 and Azure AI Services** diff --git a/app/.DS_Store b/app/.DS_Store deleted file mode 100644 index 99027d7..0000000 Binary files a/app/.DS_Store and /dev/null differ diff --git a/app/.dockerignore b/app/.dockerignore deleted file mode 100644 index bc7c0c6..0000000 --- a/app/.dockerignore +++ /dev/null @@ -1,66 +0,0 @@ -# ============================================================================= -# RFP Analyzer - Docker Ignore -# ============================================================================= -# Files and directories to exclude from Docker build context - -# Git -.git -.gitignore - -# Python -__pycache__ -*.py[cod] -*$py.class -*.so -.Python -.venv -venv -ENV -env -.eggs -*.egg-info -*.egg - -# IDE and editors -.vscode -.idea -*.swp -*.swo -*~ - -# Environment and secrets -# .env is now included for Docker builds -.env.local -.env.*.local - -# OS files -.DS_Store -Thumbs.db - -# Test files -test.http -tests/ -*.test.py -*_test.py - -# Documentation (not needed in container) -docs/ -*.md -!scoring_guide.md - -# Build artifacts -dist/ -build/ -*.whl - -# Logs -*.log -logs/ - -# uv cache (we use uv.lock but not cache) -.uv_cache - -# Misc -.coverage -.pytest_cache -htmlcov/ diff --git a/app/.env.example b/app/.env.example deleted file mode 100644 index ef1d0f1..0000000 --- a/app/.env.example +++ /dev/null @@ -1,56 +0,0 @@ -# ============================================================================= -# RFP Analyzer - Environment Configuration -# ============================================================================= -# Copy this file to .env and fill in your values -# Run 'az login' for Azure CLI authentication (recommended) -# ============================================================================= - -# ----------------------------------------------------------------------------- -# Azure AI Services / Content Understanding Configuration -# ----------------------------------------------------------------------------- -# Get these from your Azure AI Foundry resource in Azure Portal -# https://portal.azure.com -> Azure AI Foundry -> Your Resource -> Keys and Endpoint - -AZURE_CONTENT_UNDERSTANDING_ENDPOINT=https://your-ai-foundry-resource.services.ai.azure.com/ - -# Optional: API Key (only needed if NOT using Azure CLI auth via 'az login') -# AZURE_AI_API_KEY=your-api-key-here - -# ----------------------------------------------------------------------------- -# Azure Document Intelligence Configuration (Alternative to Content Understanding) -# ----------------------------------------------------------------------------- -# Get these from your Azure Document Intelligence resource in Azure Portal -# https://portal.azure.com -> Azure AI Document Intelligence -> Your Resource -> Keys and Endpoint - -# AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT=https://your-doc-intelligence.cognitiveservices.azure.com/ -# AZURE_DOCUMENT_INTELLIGENCE_KEY=your-api-key-here - -# ----------------------------------------------------------------------------- -# Azure OpenAI Configuration (Required for AI Scoring Agent) -# ----------------------------------------------------------------------------- -# Get these from your Azure OpenAI resource in Azure Portal -# https://portal.azure.com -> Azure OpenAI -> Your Resource -> Keys and Endpoint - -AZURE_OPENAI_ENDPOINT=https://your-openai-resource.openai.azure.com/ -AZURE_OPENAI_DEPLOYMENT_NAME=gpt-4o-mini - -# Optional: API Key (only needed if NOT using Azure CLI auth via 'az login') -# AZURE_OPENAI_API_KEY=your-api-key-here - -# ----------------------------------------------------------------------------- -# Azure Authentication (Optional - for Service Principal auth) -# ----------------------------------------------------------------------------- -# Only needed if NOT using Azure CLI authentication ('az login') -# Uncomment and fill these for service principal authentication - -# AZURE_TENANT_ID=your-tenant-id -# AZURE_CLIENT_ID=your-client-id -# AZURE_CLIENT_SECRET=your-client-secret - -# ----------------------------------------------------------------------------- -# Azure Application Insights (Optional - for telemetry/monitoring) -# ----------------------------------------------------------------------------- -# Get the connection string from Azure Portal -> Application Insights -> Overview -# https://portal.azure.com -> Application Insights -> Your Resource -> Connection String - -# APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=xxx;IngestionEndpoint=https://xxx.in.applicationinsights.azure.com/ diff --git a/app/.python-version b/app/.python-version deleted file mode 100644 index 24ee5b1..0000000 --- a/app/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.13 diff --git a/app/.streamlit/config.toml b/app/.streamlit/config.toml deleted file mode 100644 index 0e1462a..0000000 --- a/app/.streamlit/config.toml +++ /dev/null @@ -1,14 +0,0 @@ -[server] -# Maximum size of file uploads (in MB) -maxUploadSize = 200 - -# Maximum size of message that can be sent via websocket (in MB) -maxMessageSize = 200 - -# Enable CORS for container environments -enableCORS = false -enableXsrfProtection = false - -[browser] -# Disable file watcher in production -gatherUsageStats = false diff --git a/app/Dockerfile b/app/Dockerfile index d1d2811..8427389 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -1,68 +1,56 @@ # ============================================================================= -# RFP Analyzer - Dockerfile +# RFP Analyzer - Dockerfile (.NET 10 Blazor) # ============================================================================= # Multi-stage build for optimized image size -# Includes WeasyPrint dependencies for PDF export # ----------------------------------------------------------------------------- -# Stage 1: Build stage with uv for fast dependency installation +# Stage 1: Build stage # ----------------------------------------------------------------------------- -FROM python:3.13-slim AS builder +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -# Install uv for fast package management -COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ +WORKDIR /src -# Set working directory -WORKDIR /app +# Copy solution and project files first for better caching +COPY RfpAnalyzer.slnx ./ +COPY global.json ./ +COPY RfpAnalyzer/RfpAnalyzer.csproj ./RfpAnalyzer/ + +# Restore dependencies +RUN dotnet restore RfpAnalyzer/RfpAnalyzer.csproj -# Copy dependency files first for better caching -COPY pyproject.toml uv.lock ./ +# Copy all source code +COPY RfpAnalyzer/ ./RfpAnalyzer/ -# Install dependencies (including PDF export extras) -RUN uv sync --frozen --no-dev --all-extras +# Build and publish +RUN dotnet publish RfpAnalyzer/RfpAnalyzer.csproj -c Release -o /app/publish # ----------------------------------------------------------------------------- # Stage 2: Runtime stage # ----------------------------------------------------------------------------- -FROM python:3.13-slim AS runtime +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime -# Install system dependencies for WeasyPrint (PDF export) -RUN apt-get update && apt-get install -y --no-install-recommends \ - libpango-1.0-0 \ - libpangocairo-1.0-0 \ - libgdk-pixbuf-2.0-0 \ - libffi-dev \ - shared-mime-info \ - && rm -rf /var/lib/apt/lists/* \ - && apt-get clean +# Create non-root user for security +RUN useradd --create-home --shell /bin/bash appuser -# Set working directory WORKDIR /app -# Copy virtual environment from builder -COPY --from=builder /app/.venv /app/.venv - -# Copy application code and environment file -COPY . ./ -COPY .env ./ +# Copy published output from build stage +COPY --from=build /app/publish . # Set environment variables -ENV PATH="/app/.venv/bin:$PATH" -ENV PYTHONPATH="/app" -ENV PYTHONUNBUFFERED=1 +ENV ASPNETCORE_URLS=http://+:8501 +ENV ASPNETCORE_ENVIRONMENT=Production +ENV DOTNET_RUNNING_IN_CONTAINER=true -# Streamlit configuration -ENV STREAMLIT_SERVER_PORT=8501 -ENV STREAMLIT_SERVER_ADDRESS=0.0.0.0 -ENV STREAMLIT_SERVER_HEADLESS=true -ENV STREAMLIT_BROWSER_GATHER_USAGE_STATS=false +# Switch to non-root user +USER appuser -# Expose Streamlit port +# Expose port (matching original Streamlit port for IaC compatibility) EXPOSE 8501 # Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8501/_stcore/health')" || exit 1 +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:8501/health || exit 1 -# Run Streamlit -CMD ["streamlit", "run", "main.py", "--server.port=8501", "--server.address=0.0.0.0"] +# Run the application +ENTRYPOINT ["dotnet", "RfpAnalyzer.dll"] diff --git a/app/RfpAnalyzer.slnx b/app/RfpAnalyzer.slnx new file mode 100644 index 0000000..5899623 --- /dev/null +++ b/app/RfpAnalyzer.slnx @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/RfpAnalyzer/Components/App.razor b/app/RfpAnalyzer/Components/App.razor new file mode 100644 index 0000000..a5bcf3b --- /dev/null +++ b/app/RfpAnalyzer/Components/App.razor @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/RfpAnalyzer/Components/Layout/MainLayout.razor b/app/RfpAnalyzer/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..c63a87a --- /dev/null +++ b/app/RfpAnalyzer/Components/Layout/MainLayout.razor @@ -0,0 +1,51 @@ +@inherits LayoutComponentBase +@inject ILogger Logger + + + + + + + + + + RFP Analyzer + + + + + + + + + @Body + + + + +@code { + private bool _drawerOpen = true; + private bool _isDarkMode = false; + + private MudTheme _theme = new() + { + PaletteLight = new PaletteLight + { + Primary = "#1976D2", + Secondary = "#424242", + AppbarBackground = "#1976D2" + }, + PaletteDark = new PaletteDark + { + Primary = "#90CAF9", + Secondary = "#CE93D8", + AppbarBackground = "#1E1E2E" + } + }; + + private void ToggleDrawer() => _drawerOpen = !_drawerOpen; +} diff --git a/app/RfpAnalyzer/Components/Layout/NavMenu.razor b/app/RfpAnalyzer/Components/Layout/NavMenu.razor new file mode 100644 index 0000000..f4c786b --- /dev/null +++ b/app/RfpAnalyzer/Components/Layout/NavMenu.razor @@ -0,0 +1,7 @@ + + Home + 1. Upload Documents + 2. Extract Content + 3. Evaluate & Compare + + diff --git a/app/RfpAnalyzer/Components/Layout/ReconnectModal.razor b/app/RfpAnalyzer/Components/Layout/ReconnectModal.razor new file mode 100644 index 0000000..e740b0c --- /dev/null +++ b/app/RfpAnalyzer/Components/Layout/ReconnectModal.razor @@ -0,0 +1,31 @@ +๏ปฟ + + +
+ +

+ Rejoining the server... +

+

+ Rejoin failed... trying again in seconds. +

+

+ Failed to rejoin.
Please retry or reload the page. +

+ +

+ The session has been paused by the server. +

+

+ Failed to resume the session.
Please retry or reload the page. +

+ +
+
diff --git a/app/RfpAnalyzer/Components/Layout/ReconnectModal.razor.css b/app/RfpAnalyzer/Components/Layout/ReconnectModal.razor.css new file mode 100644 index 0000000..3ad3773 --- /dev/null +++ b/app/RfpAnalyzer/Components/Layout/ReconnectModal.razor.css @@ -0,0 +1,157 @@ +.components-reconnect-first-attempt-visible, +.components-reconnect-repeated-attempt-visible, +.components-reconnect-failed-visible, +.components-pause-visible, +.components-resume-failed-visible, +.components-rejoining-animation { + display: none; +} + +#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible, +#components-reconnect-modal.components-reconnect-show .components-rejoining-animation, +#components-reconnect-modal.components-reconnect-paused .components-pause-visible, +#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible, +#components-reconnect-modal.components-reconnect-retrying, +#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible, +#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation, +#components-reconnect-modal.components-reconnect-failed, +#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible { + display: block; +} + + +#components-reconnect-modal { + background-color: white; + width: 20rem; + margin: 20vh auto; + padding: 2rem; + border: 0; + border-radius: 0.5rem; + box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3); + opacity: 0; + transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete; + animation: components-reconnect-modal-fadeOutOpacity 0.5s both; + &[open] + +{ + animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s; + animation-fill-mode: both; +} + +} + +#components-reconnect-modal::backdrop { + background-color: rgba(0, 0, 0, 0.4); + animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; + opacity: 1; +} + +@keyframes components-reconnect-modal-slideUp { + 0% { + transform: translateY(30px) scale(0.95); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes components-reconnect-modal-fadeInOpacity { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes components-reconnect-modal-fadeOutOpacity { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +.components-reconnect-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +#components-reconnect-modal p { + margin: 0; + text-align: center; +} + +#components-reconnect-modal button { + border: 0; + background-color: #6b9ed2; + color: white; + padding: 4px 24px; + border-radius: 4px; +} + + #components-reconnect-modal button:hover { + background-color: #3b6ea2; + } + + #components-reconnect-modal button:active { + background-color: #6b9ed2; + } + +.components-rejoining-animation { + position: relative; + width: 80px; + height: 80px; +} + + .components-rejoining-animation div { + position: absolute; + border: 3px solid #0087ff; + opacity: 1; + border-radius: 50%; + animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite; + } + + .components-rejoining-animation div:nth-child(2) { + animation-delay: -0.5s; + } + +@keyframes components-rejoining-animation { + 0% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 0; + } + + 4.9% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 0; + } + + 5% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 1; + } + + 100% { + top: 0px; + left: 0px; + width: 80px; + height: 80px; + opacity: 0; + } +} diff --git a/app/RfpAnalyzer/Components/Layout/ReconnectModal.razor.js b/app/RfpAnalyzer/Components/Layout/ReconnectModal.razor.js new file mode 100644 index 0000000..a44de78 --- /dev/null +++ b/app/RfpAnalyzer/Components/Layout/ReconnectModal.razor.js @@ -0,0 +1,63 @@ +// Set up event handlers +const reconnectModal = document.getElementById("components-reconnect-modal"); +reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged); + +const retryButton = document.getElementById("components-reconnect-button"); +retryButton.addEventListener("click", retry); + +const resumeButton = document.getElementById("components-resume-button"); +resumeButton.addEventListener("click", resume); + +function handleReconnectStateChanged(event) { + if (event.detail.state === "show") { + reconnectModal.showModal(); + } else if (event.detail.state === "hide") { + reconnectModal.close(); + } else if (event.detail.state === "failed") { + document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + } else if (event.detail.state === "rejected") { + location.reload(); + } +} + +async function retry() { + document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + + try { + // Reconnect will asynchronously return: + // - true to mean success + // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID) + // - exception to mean we didn't reach the server (this can be sync or async) + const successful = await Blazor.reconnect(); + if (!successful) { + // We have been able to reach the server, but the circuit is no longer available. + // We'll reload the page so the user can continue using the app as quickly as possible. + const resumeSuccessful = await Blazor.resumeCircuit(); + if (!resumeSuccessful) { + location.reload(); + } else { + reconnectModal.close(); + } + } + } catch (err) { + // We got an exception, server is currently unavailable + document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + } +} + +async function resume() { + try { + const successful = await Blazor.resumeCircuit(); + if (!successful) { + location.reload(); + } + } catch { + reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed"); + } +} + +async function retryWhenDocumentBecomesVisible() { + if (document.visibilityState === "visible") { + await retry(); + } +} diff --git a/app/RfpAnalyzer/Components/Pages/Error.razor b/app/RfpAnalyzer/Components/Pages/Error.razor new file mode 100644 index 0000000..576cc2d --- /dev/null +++ b/app/RfpAnalyzer/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +๏ปฟ@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/app/RfpAnalyzer/Components/Pages/Evaluate.razor b/app/RfpAnalyzer/Components/Pages/Evaluate.razor new file mode 100644 index 0000000..726378a --- /dev/null +++ b/app/RfpAnalyzer/Components/Pages/Evaluate.razor @@ -0,0 +1,397 @@ +@page "/evaluate" +@using RfpAnalyzer.Models +@using System.Text.Json +@inject AppState AppState +@inject ScoringService ScoringService +@inject ComparisonService ComparisonService +@inject ISnackbar Snackbar +@inject IJSRuntime JS +@rendermode InteractiveServer + +Evaluate & Compare - RFP Analyzer + +Step 3: Evaluate & Compare + +@if (AppState.RfpDocument?.IsExtracted != true) +{ + Please extract documents first. + Go to Extract + return; +} + +@* Evaluate Button *@ +@if (!_isEvaluating && AppState.EvaluationResults.Count == 0) +{ + + Ready to Evaluate + + @AppState.ProposalDocuments.Count proposal(s) ready for evaluation against the RFP. + Reasoning effort: @AppState.ReasoningEffort. + + + Start Evaluation + + +} + +@* Progress *@ +@if (_isEvaluating) +{ + + Evaluating Proposals... + + @_currentStatus + +} + +@* Results *@ +@if (AppState.EvaluationResults.Count > 0) +{ + @* Score Overview Cards *@ + Evaluation Results + + @foreach (var kvp in AppState.EvaluationResults) + { + var result = kvp.Value; + + + + @result.SupplierName + + + + + Total Score + + @result.TotalScore.ToString("F1") + + + + Grade + + @result.Grade + + + + @if (!string.IsNullOrEmpty(result.Recommendation)) + { + @result.Recommendation + } + + + + } + + + @* Detailed Results Tabs *@ + + @foreach (var kvp in AppState.EvaluationResults) + { + var result = kvp.Value; + + @* Criterion Scores Data Grid *@ + Criterion Scores + + + + + + + + + + @* Criterion Detail Expansion Panels *@ + Detailed Analysis + + @foreach (var cs in result.CriterionScores) + { + + Justification + @cs.Justification + + @if (!string.IsNullOrEmpty(cs.Evidence)) + { + Evidence + @cs.Evidence + } + + @if (cs.Strengths.Count > 0) + { + Strengths + + @foreach (var s in cs.Strengths) + { + @s + } + + } + + @if (cs.Gaps.Count > 0) + { + Gaps + + @foreach (var g in cs.Gaps) + { + @g + } + + } + + } + + + @* Executive Summary *@ + @if (!string.IsNullOrEmpty(result.ExecutiveSummary)) + { + Executive Summary + @result.ExecutiveSummary + } + + @* Strengths & Weaknesses *@ + + + Strengths + + @foreach (var s in result.OverallStrengths) + { + @s + } + + + + Weaknesses + + @foreach (var w in result.OverallWeaknesses) + { + @w + } + + + + + @* Risk Assessment *@ + @if (!string.IsNullOrEmpty(result.RiskAssessment)) + { + Risk Assessment + @result.RiskAssessment + } + + } + + + @* Comparison Section *@ + @if (AppState.EvaluationResults.Count > 1) + { + + Vendor Comparison + + @if (AppState.ComparisonResult == null && !_isComparing) + { + + Compare Vendors + + } + + @if (_isComparing) + { + + @_comparisonStatus + } + + @if (AppState.ComparisonResult != null) + { + var comp = AppState.ComparisonResult; + + @* Rankings *@ + Rankings + + + + + + + + + + + @* Winner Summary *@ + @if (!string.IsNullOrEmpty(comp.WinnerSummary)) + { + + Winner Summary + @comp.WinnerSummary + + } + + @* Selection Recommendation *@ + @if (!string.IsNullOrEmpty(comp.SelectionRecommendation)) + { + Selection Recommendation + @comp.SelectionRecommendation + } + + @* Criterion Comparisons *@ + @if (comp.CriterionComparisons.Count > 0) + { + Performance by Criterion + + + + + + + + + + + } + + @* Key Insights *@ + @if (comp.ComparisonInsights.Count > 0) + { + Key Insights + + @foreach (var insight in comp.ComparisonInsights) + { + @insight + } + + } + } + } + + @* Export Buttons *@ + + Export Results + + JSON + CSV + Excel + +} + +@* Navigation *@ + + Back to Extract + + +@code { + private bool _isEvaluating = false; + private bool _isComparing = false; + private string _currentStatus = ""; + private string _comparisonStatus = ""; + + private async Task EvaluateAll() + { + _isEvaluating = true; + try + { + foreach (var doc in AppState.ProposalDocuments) + { + _currentStatus = $"Evaluating: {doc.FileName}..."; + StateHasChanged(); + + var result = await ScoringService.EvaluateAsync( + AppState.RfpDocument!.ExtractedContent!, + doc.ExtractedContent!, + AppState.ReasoningEffort, + status => { _currentStatus = status; InvokeAsync(StateHasChanged); }); + + AppState.EvaluationResults[doc.FileName] = result; + StateHasChanged(); + } + Snackbar.Add("All evaluations completed!", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"Evaluation error: {ex.Message}", Severity.Error); + } + finally + { + _isEvaluating = false; + _currentStatus = ""; + } + } + + private async Task RunComparison() + { + _isComparing = true; + try + { + _comparisonStatus = "Comparing vendor evaluations..."; + StateHasChanged(); + + var evaluations = AppState.EvaluationResults.Values.ToList(); + var rfpTitle = evaluations.FirstOrDefault()?.RfpTitle ?? "RFP"; + + AppState.ComparisonResult = await ComparisonService.CompareEvaluationsAsync( + evaluations, rfpTitle, AppState.ReasoningEffort, + status => { _comparisonStatus = status; InvokeAsync(StateHasChanged); }); + + Snackbar.Add("Comparison completed!", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"Comparison error: {ex.Message}", Severity.Error); + } + finally + { + _isComparing = false; + _comparisonStatus = ""; + } + } + + private async Task ExportJson() + { + var data = new + { + evaluations = AppState.EvaluationResults, + comparison = AppState.ComparisonResult + }; + var json = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }); + var bytes = System.Text.Encoding.UTF8.GetBytes(json); + await DownloadFile("rfp_analysis.json", bytes, "application/json"); + } + + private async Task ExportCsv() + { + if (AppState.ComparisonResult == null) return; + var bytes = ComparisonService.GenerateCsvReport(AppState.ComparisonResult, AppState.EvaluationResults.Values.ToList()); + await DownloadFile("rfp_comparison.csv", bytes, "text/csv"); + } + + private async Task ExportExcel() + { + if (AppState.ComparisonResult == null) return; + var bytes = ComparisonService.GenerateExcelReport(AppState.ComparisonResult, AppState.EvaluationResults.Values.ToList()); + await DownloadFile("rfp_comparison.xlsx", bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + } + + private async Task DownloadFile(string filename, byte[] content, string contentType) + { + var base64 = Convert.ToBase64String(content); + await JS.InvokeVoidAsync("downloadFile", filename, base64, contentType); + } + + private static Color GetScoreColor(double score) => score switch + { + >= 90 => Color.Success, + >= 70 => Color.Info, + >= 50 => Color.Warning, + _ => Color.Error + }; + + private static Color GetGradeColor(string grade) => grade switch + { + "A" => Color.Success, + "B" => Color.Info, + "C" => Color.Warning, + "D" => Color.Error, + _ => Color.Error + }; +} diff --git a/app/RfpAnalyzer/Components/Pages/Extract.razor b/app/RfpAnalyzer/Components/Pages/Extract.razor new file mode 100644 index 0000000..a59f727 --- /dev/null +++ b/app/RfpAnalyzer/Components/Pages/Extract.razor @@ -0,0 +1,157 @@ +@page "/extract" +@inject AppState AppState +@inject DocumentProcessorService DocumentProcessor +@inject ISnackbar Snackbar +@inject NavigationManager Navigation +@rendermode InteractiveServer + +Extract Content - RFP Analyzer + +Step 2: Extract & Configure +Configure extraction settings and process your documents. + +@if (AppState.RfpDocument == null) +{ + Please upload documents first. + Go to Upload + return; +} + + + @* Configuration *@ + + + Extraction Service + + + Content Understanding + + + Document Intelligence + + + + + + Analysis Depth + + Standard (~5 min) + Thorough (~10 min) + Comprehensive (~15 min) + + + + + + + + + @* Extraction Status *@ + + + Document Extraction + + @if (_isExtracting) + { + + @_currentStatus + } + + @* RFP Status *@ + + @(AppState.RfpDocument.IsExtracted ? "โœ…" : "โณ") RFP: @AppState.RfpDocument.FileName + @if (AppState.RfpDocument.IsExtracted) + { + โ€” @(AppState.RfpDocument.ExtractedContent?.Length ?? 0) chars extracted + } + + + @* Proposal Status *@ + @foreach (var doc in AppState.ProposalDocuments) + { + + @(doc.IsExtracted ? "โœ…" : "โณ") @doc.FileName + @if (doc.IsExtracted) + { + โ€” @(doc.ExtractedContent?.Length ?? 0) chars extracted + } + + } + + + @(_isExtracting ? "Extracting..." : "Extract Content") + + + + + +@* Navigation *@ + + + + Back to Upload + + + + Next: Evaluate + + + + + +@code { + private bool _isExtracting = false; + private string _currentStatus = ""; + + private bool AllExtracted() + { + return AppState.RfpDocument?.IsExtracted == true && + AppState.ProposalDocuments.All(p => p.IsExtracted); + } + + private async Task ExtractAllDocuments() + { + _isExtracting = true; + try + { + // Extract RFP + if (!AppState.RfpDocument!.IsExtracted) + { + _currentStatus = $"Extracting RFP: {AppState.RfpDocument.FileName}..."; + StateHasChanged(); + AppState.RfpDocument.ExtractedContent = await DocumentProcessor.ExtractContentAsync( + AppState.RfpDocument.Content, AppState.RfpDocument.FileName, AppState.SelectedService); + AppState.RfpDocument.IsExtracted = true; + StateHasChanged(); + } + + // Extract proposals + foreach (var doc in AppState.ProposalDocuments.Where(d => !d.IsExtracted)) + { + _currentStatus = $"Extracting: {doc.FileName}..."; + StateHasChanged(); + doc.ExtractedContent = await DocumentProcessor.ExtractContentAsync( + doc.Content, doc.FileName, AppState.SelectedService); + doc.IsExtracted = true; + StateHasChanged(); + } + + Snackbar.Add("All documents extracted successfully!", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"Extraction error: {ex.Message}", Severity.Error); + } + finally + { + _isExtracting = false; + _currentStatus = ""; + } + } +} diff --git a/app/RfpAnalyzer/Components/Pages/Home.razor b/app/RfpAnalyzer/Components/Pages/Home.razor new file mode 100644 index 0000000..2800d04 --- /dev/null +++ b/app/RfpAnalyzer/Components/Pages/Home.razor @@ -0,0 +1,95 @@ +@page "/" + +RFP Analyzer + + + @* Hero Section *@ + + + RFP Analyzer + + AI-Powered Proposal Analysis & Scoring + + + Upload your RFP and vendor proposals, extract content using Azure AI services, + and get intelligent scoring with multi-agent evaluation. + + + Start Analysis + + + + @* Feature Cards *@ + + + + + + Smart Extraction + + Extract content from PDFs, Word docs, and more using Azure AI Document Intelligence + or Content Understanding. + + + + + + + + + Intelligent Scoring + + Multi-agent AI system extracts criteria from RFPs and scores proposals + with detailed justifications. + + + + + + + + + Comparative Analysis + + Compare multiple vendors side by side with rankings, insights, + and selection recommendations. + + + + + + + + + Comprehensive Reports + + Export results in CSV, Excel, and JSON formats for easy sharing + and further analysis. + + + + + + + @* How It Works *@ + How It Works + + + 1. Upload Documents + Upload your RFP and one or more vendor proposals (PDF, DOCX, TXT, MD). + + + 2. Extract Content + Azure AI services extract text, tables, and structure from your documents. + + + 3. AI Evaluation + Multi-agent AI extracts criteria and scores each proposal with evidence. + + + 4. Compare & Export + Compare vendors, review rankings, and export comprehensive reports. + + + diff --git a/app/RfpAnalyzer/Components/Pages/NotFound.razor b/app/RfpAnalyzer/Components/Pages/NotFound.razor new file mode 100644 index 0000000..917ada1 --- /dev/null +++ b/app/RfpAnalyzer/Components/Pages/NotFound.razor @@ -0,0 +1,5 @@ +๏ปฟ@page "/not-found" +@layout MainLayout + +

Not Found

+

Sorry, the content you are looking for does not exist.

\ No newline at end of file diff --git a/app/RfpAnalyzer/Components/Pages/Upload.razor b/app/RfpAnalyzer/Components/Pages/Upload.razor new file mode 100644 index 0000000..bcbdc15 --- /dev/null +++ b/app/RfpAnalyzer/Components/Pages/Upload.razor @@ -0,0 +1,140 @@ +@page "/upload" +@inject AppState AppState +@inject ISnackbar Snackbar +@inject NavigationManager Navigation + +Upload Documents - RFP Analyzer + +Step 1: Upload Documents +Upload your RFP document and vendor proposals for analysis. + + + @* RFP Document Upload *@ + + + + RFP Document + + Upload the Request for Proposal document. + + + + + Upload RFP + + + + + @if (AppState.RfpDocument != null) + { + + @AppState.RfpDocument.FileName + Size: @FormatFileSize(AppState.RfpDocument.Size) + + } + + + + @* Vendor Proposals Upload *@ + + + + Vendor Proposals + + Upload one or more vendor proposals. + + + + + Upload Proposals + + + + + @if (AppState.ProposalDocuments.Count > 0) + { + + @foreach (var doc in AppState.ProposalDocuments) + { + + @doc.FileName (@FormatFileSize(doc.Size)) + + } + + } + + + + +@* Navigation *@ + + + + Back to Home + + + + Next: Extract Content + + + + + +@code { + private async Task OnRfpFileChanged(IBrowserFile file) + { + try + { + var buffer = new byte[file.Size]; + await using var stream = file.OpenReadStream(maxAllowedSize: 500 * 1024 * 1024); + await stream.ReadExactlyAsync(buffer); + + AppState.RfpDocument = new UploadedDocument + { + FileName = file.Name, + Content = buffer, + Size = file.Size + }; + Snackbar.Add($"RFP uploaded: {file.Name}", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"Error uploading RFP: {ex.Message}", Severity.Error); + } + } + + private async Task OnProposalFilesChanged(IReadOnlyList files) + { + foreach (var file in files) + { + try + { + var buffer = new byte[file.Size]; + await using var stream = file.OpenReadStream(maxAllowedSize: 500 * 1024 * 1024); + await stream.ReadExactlyAsync(buffer); + + AppState.ProposalDocuments.Add(new UploadedDocument + { + FileName = file.Name, + Content = buffer, + Size = file.Size + }); + } + catch (Exception ex) + { + Snackbar.Add($"Error uploading {file.Name}: {ex.Message}", Severity.Error); + } + } + if (files.Count > 0) + Snackbar.Add($"{files.Count} proposal(s) uploaded", Severity.Success); + } + + private static string FormatFileSize(long bytes) + { + if (bytes < 1024) return $"{bytes} B"; + if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB"; + return $"{bytes / (1024.0 * 1024.0):F1} MB"; + } +} diff --git a/app/RfpAnalyzer/Components/Routes.razor b/app/RfpAnalyzer/Components/Routes.razor new file mode 100644 index 0000000..105855d --- /dev/null +++ b/app/RfpAnalyzer/Components/Routes.razor @@ -0,0 +1,6 @@ +๏ปฟ + + + + + diff --git a/app/RfpAnalyzer/Components/_Imports.razor b/app/RfpAnalyzer/Components/_Imports.razor new file mode 100644 index 0000000..de37cc0 --- /dev/null +++ b/app/RfpAnalyzer/Components/_Imports.razor @@ -0,0 +1,14 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using RfpAnalyzer +@using RfpAnalyzer.Models +@using RfpAnalyzer.Services +@using RfpAnalyzer.Components +@using RfpAnalyzer.Components.Layout +@using MudBlazor diff --git a/app/RfpAnalyzer/Models/ComparisonModels.cs b/app/RfpAnalyzer/Models/ComparisonModels.cs new file mode 100644 index 0000000..9559d9f --- /dev/null +++ b/app/RfpAnalyzer/Models/ComparisonModels.cs @@ -0,0 +1,36 @@ +namespace RfpAnalyzer.Models; + +public class VendorRanking +{ + public int Rank { get; set; } + public string VendorName { get; set; } = ""; + public double TotalScore { get; set; } + public string Grade { get; set; } = ""; + public List KeyStrengths { get; set; } = new(); + public List KeyConcerns { get; set; } = new(); + public string Recommendation { get; set; } = ""; +} + +public class CriterionComparison +{ + public string CriterionId { get; set; } = ""; + public string CriterionName { get; set; } = ""; + public double Weight { get; set; } + public string BestVendor { get; set; } = ""; + public string WorstVendor { get; set; } = ""; + public string ScoreRange { get; set; } = ""; + public string Insights { get; set; } = ""; +} + +public class ComparisonResult +{ + public string RfpTitle { get; set; } = ""; + public string ComparisonDate { get; set; } = ""; + public int TotalVendors { get; set; } + public List VendorRankings { get; set; } = new(); + public List CriterionComparisons { get; set; } = new(); + public string WinnerSummary { get; set; } = ""; + public List ComparisonInsights { get; set; } = new(); + public string SelectionRecommendation { get; set; } = ""; + public string RiskComparison { get; set; } = ""; +} diff --git a/app/RfpAnalyzer/Models/EvaluationModels.cs b/app/RfpAnalyzer/Models/EvaluationModels.cs new file mode 100644 index 0000000..0afccad --- /dev/null +++ b/app/RfpAnalyzer/Models/EvaluationModels.cs @@ -0,0 +1,63 @@ +namespace RfpAnalyzer.Models; + +public class EvaluationResult +{ + public string RfpTitle { get; set; } = ""; + public string SupplierName { get; set; } = ""; + public string SupplierSite { get; set; } = ""; + public string ResponseId { get; set; } = ""; + public double TotalScore { get; set; } + public double ScorePercentage { get; set; } + public string Grade { get; set; } = ""; + public string Recommendation { get; set; } = ""; + public ExtractedCriteria? ExtractedCriteria { get; set; } + public List CriterionScores { get; set; } = new(); + public string ExecutiveSummary { get; set; } = ""; + public List OverallStrengths { get; set; } = new(); + public List OverallWeaknesses { get; set; } = new(); + public List Recommendations { get; set; } = new(); + public string RiskAssessment { get; set; } = ""; + public EvaluationMetadata? Metadata { get; set; } +} + +public class EvaluationMetadata +{ + public string Version { get; set; } = "2.0"; + public string EvaluationType { get; set; } = "multi-agent"; + public string EvaluationTimestamp { get; set; } = ""; + public double TotalDurationSeconds { get; set; } + public double Phase1CriteriaExtractionSeconds { get; set; } + public double Phase2ProposalScoringSeconds { get; set; } + public int CriteriaCount { get; set; } + public string ModelDeployment { get; set; } = ""; + public string ReasoningEffort { get; set; } = ""; +} + +public enum ExtractionService +{ + ContentUnderstanding, + DocumentIntelligence +} + +public class UploadedDocument +{ + public string FileName { get; set; } = ""; + public byte[] Content { get; set; } = Array.Empty(); + public long Size { get; set; } + public string? ExtractedContent { get; set; } + public bool IsExtracted { get; set; } +} + +public class AppState +{ + public UploadedDocument? RfpDocument { get; set; } + public List ProposalDocuments { get; set; } = new(); + public ExtractionService SelectedService { get; set; } = ExtractionService.ContentUnderstanding; + public string ReasoningEffort { get; set; } = "high"; + public string CustomCriteria { get; set; } = ""; + public Dictionary EvaluationResults { get; set; } = new(); + public ComparisonResult? ComparisonResult { get; set; } + public ProcessingQueue ExtractionQueue { get; set; } = new() { Name = "Extraction" }; + public ProcessingQueue EvaluationQueue { get; set; } = new() { Name = "Evaluation" }; + public int CurrentStep { get; set; } = 0; // 0=landing, 1=upload, 2=extract, 3=evaluate +} diff --git a/app/RfpAnalyzer/Models/ProcessingModels.cs b/app/RfpAnalyzer/Models/ProcessingModels.cs new file mode 100644 index 0000000..f05698a --- /dev/null +++ b/app/RfpAnalyzer/Models/ProcessingModels.cs @@ -0,0 +1,138 @@ +namespace RfpAnalyzer.Models; + +public enum QueueItemStatus +{ + Pending, + Processing, + Completed, + Failed +} + +public class QueueItem +{ + public string Id { get; set; } = ""; + public string Name { get; set; } = ""; + public string ItemType { get; set; } = ""; + public QueueItemStatus Status { get; set; } = QueueItemStatus.Pending; + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } + public double? Duration { get; set; } + public string? ErrorMessage { get; set; } + public object? Result { get; set; } + public Dictionary Metadata { get; set; } = new(); + + public void Start() + { + Status = QueueItemStatus.Processing; + StartTime = DateTime.UtcNow; + } + + public void Complete(object? result = null) + { + Status = QueueItemStatus.Completed; + EndTime = DateTime.UtcNow; + if (StartTime.HasValue) + Duration = (EndTime.Value - StartTime.Value).TotalSeconds; + Result = result; + } + + public void Fail(string errorMessage) + { + Status = QueueItemStatus.Failed; + EndTime = DateTime.UtcNow; + if (StartTime.HasValue) + Duration = (EndTime.Value - StartTime.Value).TotalSeconds; + ErrorMessage = errorMessage; + } + + public double GetElapsedTime() + { + if (Duration.HasValue) return Duration.Value; + if (StartTime.HasValue) return (DateTime.UtcNow - StartTime.Value).TotalSeconds; + return 0.0; + } + + public string GetStatusIcon() => Status switch + { + QueueItemStatus.Pending => "โณ", + QueueItemStatus.Processing => "๐Ÿ”„", + QueueItemStatus.Completed => "โœ…", + QueueItemStatus.Failed => "โŒ", + _ => "โ“" + }; +} + +public class ProcessingQueue +{ + public string Name { get; set; } = ""; + public List Items { get; set; } = new(); + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } + + public QueueItem AddItem(string id, string name, string itemType, Dictionary? metadata = null) + { + var item = new QueueItem + { + Id = id, + Name = name, + ItemType = itemType, + Metadata = metadata ?? new() + }; + Items.Add(item); + return item; + } + + public void Start() => StartTime = DateTime.UtcNow; + public void Finish() => EndTime = DateTime.UtcNow; + + public QueueItem? GetItem(string id) => Items.FirstOrDefault(i => i.Id == id); + public List GetPendingItems() => Items.Where(i => i.Status == QueueItemStatus.Pending).ToList(); + public List GetCompletedItems() => Items.Where(i => i.Status == QueueItemStatus.Completed).ToList(); + public List GetFailedItems() => Items.Where(i => i.Status == QueueItemStatus.Failed).ToList(); + + public (int Total, int Completed, int Failed, int Processing, int Pending, int Percentage) GetProgress() + { + int total = Items.Count; + int completed = Items.Count(i => i.Status == QueueItemStatus.Completed); + int failed = Items.Count(i => i.Status == QueueItemStatus.Failed); + int processing = Items.Count(i => i.Status == QueueItemStatus.Processing); + int pending = Items.Count(i => i.Status == QueueItemStatus.Pending); + int percentage = total > 0 ? (int)((completed + failed) * 100.0 / total) : 0; + return (total, completed, failed, processing, pending, percentage); + } + + public double GetTotalDuration() + { + if (!StartTime.HasValue) return 0.0; + if (EndTime.HasValue) return (EndTime.Value - StartTime.Value).TotalSeconds; + return (DateTime.UtcNow - StartTime.Value).TotalSeconds; + } + + public double GetAverageItemDuration() + { + var completed = Items.Where(i => i.Duration.HasValue).ToList(); + if (completed.Count == 0) return 0.0; + return completed.Average(i => i.Duration!.Value); + } + + public bool IsComplete => Items.All(i => i.Status == QueueItemStatus.Completed || i.Status == QueueItemStatus.Failed); + + public void Clear() + { + Items.Clear(); + StartTime = null; + EndTime = null; + } +} + +public static class DurationFormatter +{ + public static string Format(double seconds) + { + if (seconds < 1) return $"{seconds * 1000:F0}ms"; + if (seconds < 60) return $"{seconds:F1}s"; + int minutes = (int)(seconds / 60); + double remaining = seconds % 60; + return $"{minutes}m {remaining:F1}s"; + } +} diff --git a/app/RfpAnalyzer/Models/ScoringModels.cs b/app/RfpAnalyzer/Models/ScoringModels.cs new file mode 100644 index 0000000..04f6048 --- /dev/null +++ b/app/RfpAnalyzer/Models/ScoringModels.cs @@ -0,0 +1,57 @@ +namespace RfpAnalyzer.Models; + +// Maps from Python's ScoringCriterion +public class ScoringCriterion +{ + public string CriterionId { get; set; } = ""; + public string Name { get; set; } = ""; + public string Description { get; set; } = ""; + public string Category { get; set; } = ""; + public double Weight { get; set; } + public int MaxScore { get; set; } = 100; + public string EvaluationGuidance { get; set; } = ""; +} + +// Maps from Python's ExtractedCriteria +public class ExtractedCriteria +{ + public string RfpTitle { get; set; } = ""; + public string RfpSummary { get; set; } = ""; + public double TotalWeight { get; set; } = 100.0; + public List Criteria { get; set; } = new(); + public string ExtractionNotes { get; set; } = ""; +} + +// Maps from Python's CriterionScore +public class CriterionScore +{ + public string CriterionId { get; set; } = ""; + public string CriterionName { get; set; } = ""; + public double Weight { get; set; } + public double RawScore { get; set; } + public double WeightedScore { get; set; } + public string Evidence { get; set; } = ""; + public string Justification { get; set; } = ""; + public List Strengths { get; set; } = new(); + public List Gaps { get; set; } = new(); +} + +// Maps from Python's ProposalEvaluationV2 +public class ProposalEvaluation +{ + public string RfpTitle { get; set; } = ""; + public string SupplierName { get; set; } = ""; + public string SupplierSite { get; set; } = ""; + public string ResponseId { get; set; } = ""; + public string EvaluationDate { get; set; } = ""; + public double TotalScore { get; set; } + public double ScorePercentage { get; set; } + public string Grade { get; set; } = ""; + public string Recommendation { get; set; } = ""; + public List CriterionScores { get; set; } = new(); + public string ExecutiveSummary { get; set; } = ""; + public List OverallStrengths { get; set; } = new(); + public List OverallWeaknesses { get; set; } = new(); + public List Recommendations { get; set; } = new(); + public string RiskAssessment { get; set; } = ""; +} diff --git a/app/RfpAnalyzer/Program.cs b/app/RfpAnalyzer/Program.cs new file mode 100644 index 0000000..8a7e88a --- /dev/null +++ b/app/RfpAnalyzer/Program.cs @@ -0,0 +1,41 @@ +using RfpAnalyzer.Components; +using RfpAnalyzer.Models; +using RfpAnalyzer.Services; +using MudBlazor.Services; + +var builder = WebApplication.CreateBuilder(args); + +// Add configuration from environment variables +builder.Configuration.AddEnvironmentVariables(); + +// Add services to the container +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); + +builder.Services.AddMudServices(); +builder.Services.AddHttpClient(); + +// Register application services +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + app.UseHsts(); + app.UseHttpsRedirection(); +} + +app.UseAntiforgery(); +app.UseStaticFiles(); +app.MapStaticAssets(); +app.MapGet("/health", () => Results.Ok("Healthy")); +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +app.Run(); diff --git a/app/RfpAnalyzer/Properties/launchSettings.json b/app/RfpAnalyzer/Properties/launchSettings.json new file mode 100644 index 0000000..738ba08 --- /dev/null +++ b/app/RfpAnalyzer/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5128", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7080;http://localhost:5128", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/app/RfpAnalyzer/RfpAnalyzer.csproj b/app/RfpAnalyzer/RfpAnalyzer.csproj new file mode 100644 index 0000000..9fbfa8a --- /dev/null +++ b/app/RfpAnalyzer/RfpAnalyzer.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + true + + + + + + + + + + + + + + + + + + + diff --git a/app/RfpAnalyzer/Services/ComparisonService.cs b/app/RfpAnalyzer/Services/ComparisonService.cs new file mode 100644 index 0000000..35324bf --- /dev/null +++ b/app/RfpAnalyzer/Services/ComparisonService.cs @@ -0,0 +1,473 @@ +using System.Globalization; +using System.Text; +using System.Text.Json; +using Azure.AI.OpenAI; +using Azure.Identity; +using ClosedXML.Excel; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; +using OpenAI.Chat; +using RfpAnalyzer.Models; + +namespace RfpAnalyzer.Services; + +/// +/// Vendor comparison service using Microsoft Agent Framework. +/// Uses a ComparisonAgent that analyzes evaluation results from multiple vendors +/// and provides comprehensive comparative analysis. +/// +public class ComparisonService +{ + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + private const string ComparisonInstructions = """ + You are an expert procurement analyst specializing in vendor comparison and selection. + + Your task is to analyze evaluation results from multiple vendors responding to the same RFP + and provide a comprehensive comparative analysis. + + ## YOUR RESPONSIBILITIES: + + 1. **Rank Vendors**: Order vendors by total score, identifying the best performer + 2. **Compare by Criterion**: Analyze how vendors performed on each evaluation criterion + 3. **Identify Patterns**: Find strengths and weaknesses across the vendor pool + 4. **Provide Insights**: Offer actionable insights for the selection committee + 5. **Make Recommendations**: Provide clear selection recommendations + + ## ANALYSIS APPROACH: + + For each vendor: + - Review their total score and individual criterion scores + - Identify their top 3 strengths and top 3 concerns + - Assess their suitability for the project + + For the comparison: + - Identify which vendors excel in which areas + - Note any significant score gaps between vendors + - Highlight criteria where all vendors performed well or poorly + - Consider risk factors and value for money + + ## OUTPUT FORMAT: + + Respond with a valid JSON object: + + ```json + { + "rfp_title": "RFP title", + "comparison_date": "YYYY-MM-DD", + "total_vendors": , + "vendor_rankings": [ + { + "rank": 1, + "vendor_name": "Vendor Name", + "total_score": 85.5, + "grade": "B", + "key_strengths": ["strength 1", "strength 2", "strength 3"], + "key_concerns": ["concern 1", "concern 2", "concern 3"], + "recommendation": "Brief recommendation for this vendor" + } + ], + "criterion_comparisons": [ + { + "criterion_id": "C-1", + "criterion_name": "Criterion Name", + "weight": 20.0, + "best_vendor": "Best Vendor", + "worst_vendor": "Worst Vendor", + "score_range": "65-92", + "insights": "Key insight for this criterion" + } + ], + "winner_summary": "Summary of why the top vendor is recommended", + "comparison_insights": [ + "Key insight 1", + "Key insight 2", + "Key insight 3" + ], + "selection_recommendation": "Clear final recommendation with justification", + "risk_comparison": "Comparative risk assessment across vendors" + } + ``` + + ## IMPORTANT: + - Rank ALL vendors by score + - Compare ALL criteria + - Be objective and fair + - Support recommendations with evidence + - Respond with ONLY valid JSON + """; + + public ComparisonService( + IConfiguration configuration, + ILogger logger) + { + _configuration = configuration; + _logger = logger; + } + + /// + /// Creates the Azure OpenAI-backed VendorComparisonAgent using Microsoft Agent Framework. + /// This specialist agent analyzes and compares evaluation results from multiple vendors. + /// + internal AIAgent CreateComparisonAgent() + { + var chatClient = CreateAzureOpenAIChatClient(); + return chatClient.AsAIAgent( + instructions: ComparisonInstructions, + name: "VendorComparisonAgent", + description: "Analyzes evaluation results from multiple vendors and provides rankings, insights, and selection recommendations"); + } + + /// + /// Creates the ComparisonOrchestrator Agent that coordinates the comparison workflow. + /// Uses the Microsoft Agent Framework multi-agent pattern (agent-as-function-tool). + /// The orchestrator exposes the VendorComparisonAgent as a function tool via AsAIFunction(). + /// + internal AIAgent CreateComparisonOrchestrator(AIAgent comparisonAgent) + { + var chatClient = CreateAzureOpenAIChatClient(); + var comparisonFunction = comparisonAgent.AsAIFunction(); + + return chatClient.AsAIAgent( + instructions: """ + You are the Vendor Comparison Orchestrator. You coordinate comparison analysis using the VendorComparisonAgent. + + When given vendor evaluation data, you MUST: + 1. Invoke VendorComparisonAgent with the complete evaluation data + 2. Return the VendorComparisonAgent's complete JSON response exactly as received + + Do NOT modify, summarize, or reformat the agent response. + Return the comparison agent's JSON output directly. + """, + name: "ComparisonOrchestrator", + description: "Orchestrates the vendor comparison workflow", + tools: [comparisonFunction]); + } + + public async Task CompareEvaluationsAsync( + List evaluations, + string rfpTitle, + string reasoningEffort = "high", + Action? progressCallback = null, + CancellationToken ct = default) + { + _logger.LogInformation("Starting comparison of {Count} vendor evaluations using Microsoft Agent Framework", evaluations.Count); + progressCallback?.Invoke("VendorComparisonAgent preparing comparison..."); + + var agent = CreateComparisonAgent(); + var session = await agent.CreateSessionAsync(ct); + + var evaluationsSummary = FormatEvaluationsForPrompt(evaluations); + + var userPrompt = $""" + Please compare the following vendor evaluations and provide a comprehensive analysis. + + ## RFP TITLE: {rfpTitle} + + ## VENDOR EVALUATIONS: + + {evaluationsSummary} + + --- + + REQUIREMENTS: + 1. Rank all vendors by total score + 2. Compare performance on each criterion + 3. Identify the best and worst performers per criterion + 4. Provide clear selection recommendations + 5. Assess comparative risks + + Respond with ONLY valid JSON matching the schema in your instructions. + """; + + progressCallback?.Invoke("VendorComparisonAgent analyzing comparisons..."); + + var runOptions = new AgentRunOptions + { + AdditionalProperties = new AdditionalPropertiesDictionary + { + ["reasoning_effort"] = reasoningEffort + } + }; + + var response = await agent.RunAsync(userPrompt, session, runOptions, ct); + var responseText = response.Text ?? ""; + + return ParseComparisonResponse(responseText); + } + + private string FormatEvaluationsForPrompt(List evaluations) + { + var parts = new List(); + + for (int i = 0; i < evaluations.Count; i++) + { + var eval = evaluations[i]; + var sb = new StringBuilder(); + sb.AppendLine($"### Vendor {i + 1}: {eval.SupplierName}"); + sb.AppendLine($"- **Total Score:** {eval.TotalScore:F2}"); + sb.AppendLine($"- **Grade:** {eval.Grade}"); + sb.AppendLine(); + sb.AppendLine("**Criterion Scores:**"); + + foreach (var cs in eval.CriterionScores) + { + sb.AppendLine($"- {cs.CriterionName}: {cs.RawScore:F1} (weighted: {cs.WeightedScore:F2})"); + } + + if (eval.OverallStrengths.Count > 0) + sb.AppendLine($"\n**Strengths:** {string.Join(", ", eval.OverallStrengths.Take(5))}"); + if (eval.OverallWeaknesses.Count > 0) + sb.AppendLine($"\n**Weaknesses:** {string.Join(", ", eval.OverallWeaknesses.Take(5))}"); + + parts.Add(sb.ToString()); + } + + return string.Join("\n\n---\n\n", parts); + } + + private ComparisonResult ParseComparisonResponse(string responseText) + { + var text = CleanJsonResponse(responseText); + + try + { + using var doc = JsonDocument.Parse(text); + var root = doc.RootElement; + + var result = new ComparisonResult + { + RfpTitle = root.TryGetProperty("rfp_title", out var rt) ? rt.GetString() ?? "" : "", + ComparisonDate = root.TryGetProperty("comparison_date", out var cd) ? cd.GetString() ?? DateTime.UtcNow.ToString("yyyy-MM-dd") : DateTime.UtcNow.ToString("yyyy-MM-dd"), + TotalVendors = root.TryGetProperty("total_vendors", out var tv) ? tv.GetInt32() : 0, + WinnerSummary = root.TryGetProperty("winner_summary", out var ws) ? ws.GetString() ?? "" : "", + SelectionRecommendation = root.TryGetProperty("selection_recommendation", out var sr) ? sr.GetString() ?? "" : "", + RiskComparison = root.TryGetProperty("risk_comparison", out var rc) ? rc.GetString() ?? "" : "" + }; + + if (root.TryGetProperty("vendor_rankings", out var rankings)) + { + foreach (var r in rankings.EnumerateArray()) + { + result.VendorRankings.Add(new VendorRanking + { + Rank = r.TryGetProperty("rank", out var rank) ? rank.GetInt32() : 0, + VendorName = r.TryGetProperty("vendor_name", out var vn) ? vn.GetString() ?? "" : "", + TotalScore = r.TryGetProperty("total_score", out var ts) ? ts.GetDouble() : 0, + Grade = r.TryGetProperty("grade", out var g) ? g.GetString() ?? "" : "", + KeyStrengths = r.TryGetProperty("key_strengths", out var ks) ? ks.EnumerateArray().Select(s => s.GetString() ?? "").ToList() : new(), + KeyConcerns = r.TryGetProperty("key_concerns", out var kc) ? kc.EnumerateArray().Select(s => s.GetString() ?? "").ToList() : new(), + Recommendation = r.TryGetProperty("recommendation", out var rec) ? rec.GetString() ?? "" : "" + }); + } + } + + if (root.TryGetProperty("criterion_comparisons", out var comparisons)) + { + foreach (var c in comparisons.EnumerateArray()) + { + result.CriterionComparisons.Add(new CriterionComparison + { + CriterionId = c.TryGetProperty("criterion_id", out var cid) ? cid.GetString() ?? "" : "", + CriterionName = c.TryGetProperty("criterion_name", out var cn) ? cn.GetString() ?? "" : "", + Weight = c.TryGetProperty("weight", out var w) ? w.GetDouble() : 0, + BestVendor = c.TryGetProperty("best_vendor", out var bv) ? bv.GetString() ?? "" : "", + WorstVendor = c.TryGetProperty("worst_vendor", out var wv) ? wv.GetString() ?? "" : "", + ScoreRange = c.TryGetProperty("score_range", out var sr2) ? sr2.GetString() ?? "" : "", + Insights = c.TryGetProperty("insights", out var ins) ? ins.GetString() ?? "" : "" + }); + } + } + + if (root.TryGetProperty("comparison_insights", out var insights)) + { + result.ComparisonInsights = insights.EnumerateArray().Select(s => s.GetString() ?? "").ToList(); + } + + return result; + } + catch (JsonException ex) + { + _logger.LogError(ex, "Failed to parse comparison JSON"); + return new ComparisonResult + { + RfpTitle = "Unknown RFP", + ComparisonDate = DateTime.UtcNow.ToString("yyyy-MM-dd"), + WinnerSummary = "Error parsing comparison results", + SelectionRecommendation = "Unable to provide recommendation due to parsing error" + }; + } + } + + public byte[] GenerateCsvReport(ComparisonResult comparison, List evaluations) + { + using var ms = new MemoryStream(); + using var writer = new StreamWriter(ms, Encoding.UTF8); + + writer.WriteLine("RFP Comparison Report"); + writer.WriteLine($"RFP Title,{comparison.RfpTitle}"); + writer.WriteLine($"Comparison Date,{comparison.ComparisonDate}"); + writer.WriteLine($"Total Vendors,{comparison.TotalVendors}"); + writer.WriteLine(); + + writer.WriteLine("=== VENDOR RANKINGS ==="); + writer.WriteLine("Rank,Vendor Name,Total Score,Grade,Recommendation"); + foreach (var ranking in comparison.VendorRankings) + { + writer.WriteLine($"{ranking.Rank},{ranking.VendorName},{ranking.TotalScore:F2},{ranking.Grade},{EscapeCsv(ranking.Recommendation)}"); + } + writer.WriteLine(); + + writer.WriteLine("=== CRITERION COMPARISON ==="); + var header = "Criterion,Weight"; + foreach (var eval in evaluations) + { + header += $",{eval.SupplierName}"; + } + writer.WriteLine(header); + + if (evaluations.Count > 0) + { + var allCriteria = evaluations[0].CriterionScores; + for (int idx = 0; idx < allCriteria.Count; idx++) + { + var row = $"{allCriteria[idx].CriterionName},{allCriteria[idx].Weight:F1}%"; + foreach (var eval in evaluations) + { + if (idx < eval.CriterionScores.Count) + row += $",{eval.CriterionScores[idx].RawScore:F1}"; + else + row += ","; + } + writer.WriteLine(row); + } + + var totalRow = "TOTAL SCORE,100%"; + foreach (var eval in evaluations) + { + totalRow += $",{eval.TotalScore:F2}"; + } + writer.WriteLine(totalRow); + } + + writer.WriteLine(); + writer.WriteLine("=== KEY INSIGHTS ==="); + foreach (var insight in comparison.ComparisonInsights) + { + writer.WriteLine(EscapeCsv(insight)); + } + + writer.WriteLine(); + writer.WriteLine("=== SELECTION RECOMMENDATION ==="); + writer.WriteLine(EscapeCsv(comparison.SelectionRecommendation)); + + writer.Flush(); + return ms.ToArray(); + } + + public byte[] GenerateExcelReport(ComparisonResult comparison, List evaluations) + { + using var workbook = new XLWorkbook(); + + // Rankings sheet + var rankingsSheet = workbook.Worksheets.Add("Rankings"); + rankingsSheet.Cell(1, 1).Value = "Rank"; + rankingsSheet.Cell(1, 2).Value = "Vendor"; + rankingsSheet.Cell(1, 3).Value = "Score"; + rankingsSheet.Cell(1, 4).Value = "Grade"; + rankingsSheet.Cell(1, 5).Value = "Recommendation"; + var headerRange = rankingsSheet.Range(1, 1, 1, 5); + headerRange.Style.Font.Bold = true; + + for (int i = 0; i < comparison.VendorRankings.Count; i++) + { + var r = comparison.VendorRankings[i]; + rankingsSheet.Cell(i + 2, 1).Value = r.Rank; + rankingsSheet.Cell(i + 2, 2).Value = r.VendorName; + rankingsSheet.Cell(i + 2, 3).Value = r.TotalScore; + rankingsSheet.Cell(i + 2, 4).Value = r.Grade; + rankingsSheet.Cell(i + 2, 5).Value = r.Recommendation; + } + rankingsSheet.Columns().AdjustToContents(); + + // Score comparison sheet + if (evaluations.Count > 0) + { + var scoresSheet = workbook.Worksheets.Add("Score Comparison"); + scoresSheet.Cell(1, 1).Value = "Criterion"; + scoresSheet.Cell(1, 2).Value = "Weight"; + for (int i = 0; i < evaluations.Count; i++) + { + scoresSheet.Cell(1, i + 3).Value = evaluations[i].SupplierName; + } + var scoreHeader = scoresSheet.Range(1, 1, 1, evaluations.Count + 2); + scoreHeader.Style.Font.Bold = true; + + var criteria = evaluations[0].CriterionScores; + for (int idx = 0; idx < criteria.Count; idx++) + { + scoresSheet.Cell(idx + 2, 1).Value = criteria[idx].CriterionName; + scoresSheet.Cell(idx + 2, 2).Value = $"{criteria[idx].Weight:F1}%"; + for (int e = 0; e < evaluations.Count; e++) + { + if (idx < evaluations[e].CriterionScores.Count) + scoresSheet.Cell(idx + 2, e + 3).Value = evaluations[e].CriterionScores[idx].RawScore; + } + } + + var totalRow = criteria.Count + 2; + scoresSheet.Cell(totalRow, 1).Value = "TOTAL"; + scoresSheet.Cell(totalRow, 2).Value = "100%"; + for (int e = 0; e < evaluations.Count; e++) + { + scoresSheet.Cell(totalRow, e + 3).Value = evaluations[e].TotalScore; + } + scoresSheet.Row(totalRow).Style.Font.Bold = true; + scoresSheet.Columns().AdjustToContents(); + } + + using var ms = new MemoryStream(); + workbook.SaveAs(ms); + return ms.ToArray(); + } + + private static string EscapeCsv(string value) + { + if (string.IsNullOrEmpty(value)) return ""; + if (value.Contains(',') || value.Contains('"') || value.Contains('\n')) + { + return $"\"{value.Replace("\"", "\"\"")}\""; + } + return value; + } + + private OpenAI.Chat.ChatClient CreateAzureOpenAIChatClient() + { + var endpoint = _configuration["AZURE_OPENAI_ENDPOINT"]; + if (string.IsNullOrWhiteSpace(endpoint)) + throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not configured. Set it in appsettings.json, appsettings.Development.json, or as an environment variable."); + var deploymentName = _configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]; + if (string.IsNullOrWhiteSpace(deploymentName)) + throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not configured. Set it in appsettings.json, appsettings.Development.json, or as an environment variable."); + + // AzureOpenAIClient expects the base endpoint without path segments like /openai/ + var endpointUri = new Uri(endpoint); + var baseEndpoint = new Uri($"{endpointUri.Scheme}://{endpointUri.Host}/"); + + var client = new AzureOpenAIClient( + baseEndpoint, + new DefaultAzureCredential()); + + return client.GetChatClient(deploymentName); + } + + internal static string CleanJsonResponse(string text) + { + text = text.Trim(); + if (text.StartsWith("```json")) text = text[7..]; + else if (text.StartsWith("```")) text = text[3..]; + if (text.EndsWith("```")) text = text[..^3]; + return text.Trim(); + } +} diff --git a/app/RfpAnalyzer/Services/DocumentProcessorService.cs b/app/RfpAnalyzer/Services/DocumentProcessorService.cs new file mode 100644 index 0000000..ac92475 --- /dev/null +++ b/app/RfpAnalyzer/Services/DocumentProcessorService.cs @@ -0,0 +1,191 @@ +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure; +using Azure.AI.DocumentIntelligence; +using Azure.Core; +using Azure.Identity; +using RfpAnalyzer.Models; + +namespace RfpAnalyzer.Services; + +public class DocumentProcessorService +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + private readonly TokenCredential _credential; + + public DocumentProcessorService( + IHttpClientFactory httpClientFactory, + IConfiguration configuration, + ILogger logger) + { + _httpClientFactory = httpClientFactory; + _configuration = configuration; + _logger = logger; + _credential = new DefaultAzureCredential(); + } + + public async Task ExtractContentAsync(byte[] fileBytes, string filename, ExtractionService service, CancellationToken ct = default) + { + var requestId = Guid.NewGuid().ToString()[..8]; + _logger.LogInformation("[REQ:{RequestId}] Starting content extraction for: {Filename} ({Size} bytes) using {Service}", + requestId, filename, fileBytes.Length, service); + + var extension = Path.GetExtension(filename).TrimStart('.').ToLowerInvariant(); + + // Handle plain text and markdown files directly + if (extension is "txt" or "md") + { + _logger.LogInformation("[REQ:{RequestId}] Processing as plain text/markdown", requestId); + return Encoding.UTF8.GetString(fileBytes); + } + + return service switch + { + ExtractionService.ContentUnderstanding => await ExtractWithContentUnderstandingAsync(fileBytes, filename, requestId, ct), + ExtractionService.DocumentIntelligence => await ExtractWithDocumentIntelligenceAsync(fileBytes, requestId, ct), + _ => throw new ArgumentOutOfRangeException(nameof(service)) + }; + } + + /// + /// Maps a file extension to the corresponding MIME type for Content Understanding. + /// + private static string GetMimeType(string filename) + { + var ext = Path.GetExtension(filename).ToLowerInvariant(); + return ext switch + { + ".pdf" => "application/pdf", + ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ".doc" => "application/msword", + ".pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation", + ".ppt" => "application/vnd.ms-powerpoint", + ".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ".xls" => "application/vnd.ms-excel", + ".png" => "image/png", + ".jpg" or ".jpeg" => "image/jpeg", + ".tiff" or ".tif" => "image/tiff", + ".bmp" => "image/bmp", + ".html" or ".htm" => "text/html", + _ => "application/octet-stream" + }; + } + + private async Task ExtractWithContentUnderstandingAsync(byte[] fileBytes, string filename, string requestId, CancellationToken ct) + { + var endpoint = _configuration["AZURE_CONTENT_UNDERSTANDING_ENDPOINT"]; + if (string.IsNullOrWhiteSpace(endpoint)) + throw new InvalidOperationException("AZURE_CONTENT_UNDERSTANDING_ENDPOINT is not configured. Set it in appsettings.json, appsettings.Development.json, or as an environment variable."); + + _logger.LogInformation("[REQ:{RequestId}] Processing with Azure Content Understanding...", requestId); + + var client = _httpClientFactory.CreateClient(); + var token = await GetTokenAsync(ct); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Step 1: Begin analysis โ€” send as JSON with base64 data URI + var analyzeUrl = $"{endpoint.TrimEnd('/')}/contentunderstanding/analyzers/prebuilt-read:analyze?api-version=2024-12-01-preview"; + + var mimeType = GetMimeType(filename); + var base64Content = Convert.ToBase64String(fileBytes); + var dataUri = $"data:{mimeType};base64,{base64Content}"; + + var requestBody = JsonSerializer.Serialize(new { url = dataUri }); + using var content = new StringContent(requestBody, Encoding.UTF8, "application/json"); + + var response = await client.PostAsync(analyzeUrl, content, ct); + if (!response.IsSuccessStatusCode) + { + var errorBody = await response.Content.ReadAsStringAsync(ct); + _logger.LogError("[REQ:{RequestId}] Content Understanding returned {StatusCode}: {Error}", requestId, response.StatusCode, errorBody); + throw new HttpRequestException($"Content Understanding analysis failed ({response.StatusCode}): {errorBody}"); + } + + // Step 2: Poll for result + var operationLocation = response.Headers.GetValues("Operation-Location").FirstOrDefault() + ?? throw new InvalidOperationException("No Operation-Location header in response"); + + _logger.LogInformation("[REQ:{RequestId}] Polling for analysis result...", requestId); + + string? markdown = null; + for (int i = 0; i < 120; i++) // Poll for up to 10 minutes + { + await Task.Delay(5000, ct); + + var pollClient = _httpClientFactory.CreateClient(); + var pollToken = await GetTokenAsync(ct); + pollClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", pollToken); + + var pollResponse = await pollClient.GetAsync(operationLocation, ct); + pollResponse.EnsureSuccessStatusCode(); + + var pollJson = await pollResponse.Content.ReadAsStringAsync(ct); + using var doc = JsonDocument.Parse(pollJson); + var status = doc.RootElement.GetProperty("status").GetString(); + + if (status == "Succeeded" || status == "succeeded") + { + if (doc.RootElement.TryGetProperty("result", out var result) && + result.TryGetProperty("contents", out var contents) && + contents.GetArrayLength() > 0) + { + var first = contents[0]; + if (first.TryGetProperty("markdown", out var md)) + { + markdown = md.GetString() ?? ""; + } + } + break; + } + else if (status == "Failed" || status == "failed") + { + throw new InvalidOperationException($"Content Understanding analysis failed: {pollJson}"); + } + } + + _logger.LogInformation("[REQ:{RequestId}] Content Understanding extraction completed ({Chars} chars)", requestId, markdown?.Length ?? 0); + return markdown ?? ""; + } + + /// + /// Extracts document content using the official Azure.AI.DocumentIntelligence SDK. + /// Uses the prebuilt-layout model with markdown output format. + /// Authentication via DefaultAzureCredential (Managed Identity in Azure, az login locally). + /// + private async Task ExtractWithDocumentIntelligenceAsync(byte[] fileBytes, string requestId, CancellationToken ct) + { + var endpoint = _configuration["AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT"]; + if (string.IsNullOrWhiteSpace(endpoint)) + endpoint = _configuration["AZURE_CONTENT_UNDERSTANDING_ENDPOINT"]; + if (string.IsNullOrWhiteSpace(endpoint)) + throw new InvalidOperationException("AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT is not configured. Set it in appsettings.json, appsettings.Development.json, or as an environment variable."); + + _logger.LogInformation("[REQ:{RequestId}] Processing with Azure Document Intelligence SDK...", requestId); + + var client = new DocumentIntelligenceClient(new Uri(endpoint), _credential); + + var analyzeOptions = new AnalyzeDocumentOptions("prebuilt-layout", BinaryData.FromBytes(fileBytes)) + { + OutputContentFormat = DocumentContentFormat.Markdown + }; + + _logger.LogInformation("[REQ:{RequestId}] Starting document analysis (prebuilt-layout, markdown output)...", requestId); + var operation = await client.AnalyzeDocumentAsync(WaitUntil.Completed, analyzeOptions, ct); + var result = operation.Value; + + var content = result.Content ?? ""; + _logger.LogInformation("[REQ:{RequestId}] Document Intelligence extraction completed ({Chars} chars)", requestId, content.Length); + return content; + } + + private async Task GetTokenAsync(CancellationToken ct) + { + var tokenResult = await _credential.GetTokenAsync( + new TokenRequestContext(new[] { "https://cognitiveservices.azure.com/.default" }), ct); + return tokenResult.Token; + } +} diff --git a/app/RfpAnalyzer/Services/ScoringService.cs b/app/RfpAnalyzer/Services/ScoringService.cs new file mode 100644 index 0000000..26cdeb2 --- /dev/null +++ b/app/RfpAnalyzer/Services/ScoringService.cs @@ -0,0 +1,568 @@ +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; +using OpenAI.Chat; +using RfpAnalyzer.Models; +using System.Text.Json; + +namespace RfpAnalyzer.Services; + +/// +/// Multi-agent scoring service using Microsoft Agent Framework. +/// Uses two specialized agents: +/// 1. CriteriaExtractionAgent - Analyzes RFPs and extracts scoring criteria +/// 2. ProposalScoringAgent - Scores vendor proposals against extracted criteria +/// +public class ScoringService +{ + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + private const string CriteriaExtractionInstructions = """ + You are an expert procurement analyst specializing in RFP (Request for Proposal) analysis. + + Your task is to carefully analyze RFP documents and extract comprehensive scoring criteria that will be used to evaluate vendor proposals. + + ## YOUR RESPONSIBILITIES: + + 1. **Identify Evaluation Criteria**: Find all evaluation criteria mentioned in the RFP, including: + - Explicitly stated criteria (often in "Evaluation Criteria" or "Selection Criteria" sections) + - Implied criteria based on requirements and priorities + - Industry-standard criteria relevant to the type of work + + 2. **Assign Weights**: Distribute 100 total weight points across criteria based on: + - Explicit weights mentioned in the RFP + - Emphasis and priority indicated in the document + - Industry standards for similar projects + - Balanced evaluation across technical, financial, and qualitative factors + + 3. **Provide Evaluation Guidance**: For each criterion, explain: + - What constitutes excellent performance (90-100 score) + - What constitutes good performance (70-89 score) + - What constitutes acceptable performance (50-69 score) + - What constitutes poor performance (below 50) + + ## WEIGHT DISTRIBUTION GUIDELINES: + + - Technical capabilities: typically 30-50% + - Experience and track record: typically 15-25% + - Methodology and approach: typically 15-25% + - Pricing/value: typically 15-30% (if mentioned) + - Team qualifications: typically 10-20% + + ## OUTPUT REQUIREMENTS: + + You MUST respond with a valid JSON object matching this exact structure: + + ```json + { + "rfp_title": "Extracted RFP title", + "rfp_summary": "2-3 sentence summary of what the RFP is requesting", + "total_weight": 100.0, + "criteria": [ + { + "criterion_id": "C-1", + "name": "Criterion Name", + "description": "Detailed description of what this criterion evaluates", + "category": "Technical|Financial|Experience|Qualitative", + "weight": , + "max_score": 100, + "evaluation_guidance": "Detailed guidance on how to score this criterion" + } + ], + "extraction_notes": "Notes about how criteria were identified and weighted" + } + ``` + + ## IMPORTANT: + - All weights MUST sum to exactly 100 + - Include at least 4-8 meaningful criteria + - Be specific in descriptions and guidance + - Respond with ONLY valid JSON, no additional text + """; + + private const string ProposalScoringInstructionsTemplate = """ + You are an expert procurement evaluator with extensive experience scoring vendor proposals. + + Your task is to objectively evaluate a vendor proposal against specific scoring criteria extracted from an RFP. + + ## SCORING METHODOLOGY: + + For EACH criterion, you must: + + 1. **Find Evidence**: Locate relevant content in the proposal + 2. **Assess Quality**: Compare against the evaluation guidance + 3. **Assign Score**: Score 0-100 based on: + - 90-100 (Excellent): Exceeds requirements, exceptional quality + - 70-89 (Good): Fully meets requirements, high quality + - 50-69 (Acceptable): Meets minimum requirements + - 30-49 (Below Average): Partially meets requirements + - 0-29 (Poor): Fails to meet requirements + + 4. **Calculate Weighted Score**: weighted_score = (raw_score * weight) / 100 + + 5. **Document Everything**: Provide evidence, justification, strengths, and gaps + + ## EVALUATION CRITERIA TO USE: + + {0} + + ## GRADE ASSIGNMENT: + + Based on total weighted score: + - A: 90-100 + - B: 80-89 + - C: 70-79 + - D: 60-69 + - F: Below 60 + + ## OUTPUT FORMAT: + + You MUST respond with a valid JSON object: + + ```json + {{ + "rfp_title": "RFP title", + "supplier_name": "Extracted vendor name", + "supplier_site": "Vendor location", + "response_id": "Generate ID like RESP-2025-XXXX", + "evaluation_date": "YYYY-MM-DD", + "total_score": , + "score_percentage": , + "grade": "A/B/C/D/F", + "recommendation": "Clear recommendation statement", + "criterion_scores": [ + {{ + "criterion_id": "C-1", + "criterion_name": "Criterion Name", + "weight": , + "raw_score": <0-100>, + "weighted_score": , + "evidence": "Specific evidence from proposal", + "justification": "Detailed scoring justification", + "strengths": ["strength1", "strength2"], + "gaps": ["gap1", "gap2"] + }} + ], + "executive_summary": "2-3 paragraph executive summary", + "overall_strengths": ["key strength 1", "key strength 2"], + "overall_weaknesses": ["key weakness 1", "key weakness 2"], + "recommendations": ["recommendation 1", "recommendation 2"], + "risk_assessment": "Assessment of risks with this vendor" + }} + ``` + + ## IMPORTANT: + - Score EVERY criterion from the provided list + - Provide specific evidence from the proposal + - Be objective and fair + - Respond with ONLY valid JSON + """; + + public ScoringService( + IConfiguration configuration, + ILogger logger) + { + _configuration = configuration; + _logger = logger; + } + + /// + /// Creates the Azure OpenAI-backed CriteriaExtractionAgent using Microsoft Agent Framework. + /// This agent is responsible for analyzing RFP documents and extracting scoring criteria. + /// + internal AIAgent CreateCriteriaExtractionAgent() + { + var chatClient = CreateAzureOpenAIChatClient(); + return chatClient.AsAIAgent( + instructions: CriteriaExtractionInstructions, + name: "CriteriaExtractionAgent", + description: "Analyzes RFP documents and extracts comprehensive scoring criteria with weights"); + } + + /// + /// Creates the Azure OpenAI-backed ProposalScoringAgent using Microsoft Agent Framework. + /// This agent scores vendor proposals against the extracted criteria. + /// + internal AIAgent CreateProposalScoringAgent(string criteriaJson) + { + var systemInstructions = string.Format(ProposalScoringInstructionsTemplate, criteriaJson); + var chatClient = CreateAzureOpenAIChatClient(); + return chatClient.AsAIAgent( + instructions: systemInstructions, + name: "ProposalScoringAgent", + description: "Scores a vendor proposal against extracted RFP criteria and provides detailed evaluation"); + } + + /// + /// Creates the Orchestrator Agent that coordinates between CriteriaExtractionAgent and ProposalScoringAgent. + /// Uses the Microsoft Agent Framework multi-agent pattern (agent-as-function-tool). + /// The orchestrator exposes the specialized agents as function tools via AsAIFunction() + /// and delegates work to them as appropriate. + /// + internal AIAgent CreateOrchestratorAgent(AIAgent criteriaAgent, AIAgent scoringAgent) + { + var chatClient = CreateAzureOpenAIChatClient(); + + // Expose each specialist agent as a callable function tool for the orchestrator + var criteriaFunction = criteriaAgent.AsAIFunction(); + var scoringFunction = scoringAgent.AsAIFunction(); + + return chatClient.AsAIAgent( + instructions: """ + You are the RFP Evaluation Orchestrator. You coordinate between two specialist agents: + + 1. **CriteriaExtractionAgent** - Call this first to extract scoring criteria from the RFP document. + Pass the full RFP content. It returns JSON with criteria, weights, and evaluation guidance. + + 2. **ProposalScoringAgent** - Call this second to score a vendor proposal against criteria. + Pass the vendor proposal content along with the RFP context. It returns JSON with scores and analysis. + + When given an evaluation request, you MUST: + 1. First invoke CriteriaExtractionAgent with the RFP content + 2. Then invoke ProposalScoringAgent with the proposal content and RFP context + 3. Return the ProposalScoringAgent's complete JSON response exactly as received + + Do NOT modify, summarize, or reformat the agent responses. + Return the final scoring agent's JSON output directly. + """, + name: "RfpEvaluationOrchestrator", + description: "Orchestrates the multi-agent RFP evaluation workflow", + tools: [criteriaFunction, scoringFunction]); + } + + public async Task EvaluateAsync( + string rfpContent, + string proposalContent, + string reasoningEffort = "high", + Action? progressCallback = null, + CancellationToken ct = default) + { + var totalStart = DateTime.UtcNow; + _logger.LogInformation("Multi-Agent evaluation started using Microsoft Agent Framework (effort: {Effort})", reasoningEffort); + + // Phase 1: CriteriaExtractionAgent extracts criteria from RFP + progressCallback?.Invoke("Phase 1: CriteriaExtractionAgent analyzing RFP..."); + var phase1Start = DateTime.UtcNow; + var criteria = await ExtractCriteriaWithAgentAsync(rfpContent, reasoningEffort, progressCallback, ct); + var phase1Duration = (DateTime.UtcNow - phase1Start).TotalSeconds; + _logger.LogInformation("Phase 1 completed in {Duration:F2}s - Extracted {Count} criteria", + phase1Duration, criteria.Criteria.Count); + + // Phase 2: ProposalScoringAgent scores proposal against extracted criteria + progressCallback?.Invoke("Phase 2: ProposalScoringAgent scoring proposal..."); + var phase2Start = DateTime.UtcNow; + var evaluation = await ScoreProposalWithAgentAsync(criteria, proposalContent, reasoningEffort, progressCallback, ct); + var phase2Duration = (DateTime.UtcNow - phase2Start).TotalSeconds; + _logger.LogInformation("Phase 2 completed in {Duration:F2}s - Total score: {Score:F2}", + phase2Duration, evaluation.TotalScore); + + var totalDuration = (DateTime.UtcNow - totalStart).TotalSeconds; + + var result = new EvaluationResult + { + RfpTitle = evaluation.RfpTitle, + SupplierName = evaluation.SupplierName, + SupplierSite = evaluation.SupplierSite, + ResponseId = evaluation.ResponseId, + TotalScore = evaluation.TotalScore, + ScorePercentage = evaluation.ScorePercentage, + Grade = evaluation.Grade, + Recommendation = evaluation.Recommendation, + ExtractedCriteria = criteria, + CriterionScores = evaluation.CriterionScores, + ExecutiveSummary = evaluation.ExecutiveSummary, + OverallStrengths = evaluation.OverallStrengths, + OverallWeaknesses = evaluation.OverallWeaknesses, + Recommendations = evaluation.Recommendations, + RiskAssessment = evaluation.RiskAssessment, + Metadata = new EvaluationMetadata + { + Version = "3.0", + EvaluationType = "microsoft-agent-framework", + EvaluationTimestamp = DateTime.UtcNow.ToString("o"), + TotalDurationSeconds = Math.Round(totalDuration, 2), + Phase1CriteriaExtractionSeconds = Math.Round(phase1Duration, 2), + Phase2ProposalScoringSeconds = Math.Round(phase2Duration, 2), + CriteriaCount = criteria.Criteria.Count, + ModelDeployment = _configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? string.Empty, + ReasoningEffort = reasoningEffort + } + }; + + _logger.LogInformation("Multi-Agent evaluation completed in {Duration:F2}s", totalDuration); + return result; + } + + private async Task ExtractCriteriaWithAgentAsync( + string rfpContent, string reasoningEffort, Action? progressCallback, CancellationToken ct) + { + progressCallback?.Invoke("CriteriaExtractionAgent analyzing RFP structure..."); + + var agent = CreateCriteriaExtractionAgent(); + var session = await agent.CreateSessionAsync(ct); + + var userPrompt = $""" + Please analyze the following RFP document and extract comprehensive scoring criteria. + + ## RFP DOCUMENT: + + {rfpContent} + + --- + + REQUIREMENTS: + 1. Identify all evaluation criteria (explicit and implied) + 2. Assign weights that sum to exactly 100 + 3. Provide detailed evaluation guidance for each criterion + 4. Include the RFP title and a brief summary + + Respond with ONLY valid JSON matching the schema in your instructions. + """; + + progressCallback?.Invoke("CriteriaExtractionAgent extracting criteria..."); + + var runOptions = new AgentRunOptions + { + AdditionalProperties = new AdditionalPropertiesDictionary + { + ["reasoning_effort"] = reasoningEffort + } + }; + + var response = await agent.RunAsync(userPrompt, session, runOptions, ct); + var responseText = response.Text ?? ""; + + return ParseCriteriaResponse(responseText); + } + + private async Task ScoreProposalWithAgentAsync( + ExtractedCriteria criteria, string proposalContent, string reasoningEffort, + Action? progressCallback, CancellationToken ct) + { + progressCallback?.Invoke("ProposalScoringAgent preparing scoring framework..."); + + var criteriaJson = JsonSerializer.Serialize(criteria.Criteria.Select(c => new + { + criterion_id = c.CriterionId, + name = c.Name, + description = c.Description, + category = c.Category, + weight = c.Weight, + max_score = c.MaxScore, + evaluation_guidance = c.EvaluationGuidance + }), new JsonSerializerOptions { WriteIndented = true }); + + var agent = CreateProposalScoringAgent(criteriaJson); + var session = await agent.CreateSessionAsync(ct); + + var userPrompt = $""" + Please evaluate the following vendor proposal against the scoring criteria. + + ## RFP CONTEXT: + - Title: {criteria.RfpTitle} + - Summary: {criteria.RfpSummary} + + ## VENDOR PROPOSAL: + + {proposalContent} + + --- + + REQUIREMENTS: + 1. Score each criterion from 0-100 + 2. Calculate weighted scores + 3. Provide evidence and justification for each score + 4. Summarize strengths, weaknesses, and recommendations + 5. Assign an overall grade + + Respond with ONLY valid JSON matching the schema in your instructions. + """; + + progressCallback?.Invoke("ProposalScoringAgent scoring proposal..."); + + var runOptions = new AgentRunOptions + { + AdditionalProperties = new AdditionalPropertiesDictionary + { + ["reasoning_effort"] = reasoningEffort + } + }; + + var response = await agent.RunAsync(userPrompt, session, runOptions, ct); + var responseText = response.Text ?? ""; + + return ParseScoringResponse(responseText, criteria); + } + + private OpenAI.Chat.ChatClient CreateAzureOpenAIChatClient() + { + var endpoint = _configuration["AZURE_OPENAI_ENDPOINT"]; + if (string.IsNullOrWhiteSpace(endpoint)) + throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not configured. Set it in appsettings.json, appsettings.Development.json, or as an environment variable."); + var deploymentName = _configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]; + if (string.IsNullOrWhiteSpace(deploymentName)) + throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not configured. Set it in appsettings.json, appsettings.Development.json, or as an environment variable."); + + // AzureOpenAIClient expects the base endpoint without path segments like /openai/ + var endpointUri = new Uri(endpoint); + var baseEndpoint = new Uri($"{endpointUri.Scheme}://{endpointUri.Host}/"); + + var client = new AzureOpenAIClient( + baseEndpoint, + new DefaultAzureCredential()); + + return client.GetChatClient(deploymentName); + } + + internal ExtractedCriteria ParseCriteriaResponse(string responseText) + { + var text = CleanJsonResponse(responseText); + + try + { + using var doc = JsonDocument.Parse(text); + var root = doc.RootElement; + + var criteria = new ExtractedCriteria + { + RfpTitle = root.GetProperty("rfp_title").GetString() ?? "Unknown RFP", + RfpSummary = root.GetProperty("rfp_summary").GetString() ?? "", + ExtractionNotes = root.TryGetProperty("extraction_notes", out var notes) ? notes.GetString() ?? "" : "" + }; + + if (root.TryGetProperty("criteria", out var criteriaArray)) + { + foreach (var c in criteriaArray.EnumerateArray()) + { + criteria.Criteria.Add(new ScoringCriterion + { + CriterionId = c.GetProperty("criterion_id").GetString() ?? "", + Name = c.GetProperty("name").GetString() ?? "", + Description = c.TryGetProperty("description", out var desc) ? desc.GetString() ?? "" : "", + Category = c.TryGetProperty("category", out var cat) ? cat.GetString() ?? "" : "", + Weight = c.GetProperty("weight").GetDouble(), + MaxScore = c.TryGetProperty("max_score", out var ms) ? ms.GetInt32() : 100, + EvaluationGuidance = c.TryGetProperty("evaluation_guidance", out var eg) ? eg.GetString() ?? "" : "" + }); + } + } + + // Normalize weights + var totalWeight = criteria.Criteria.Sum(c => c.Weight); + if (criteria.Criteria.Count > 0 && Math.Abs(totalWeight - 100) > 0.1) + { + _logger.LogWarning("Normalizing weights from {Total} to 100", totalWeight); + foreach (var c in criteria.Criteria) + { + c.Weight = c.Weight / totalWeight * 100; + } + } + criteria.TotalWeight = 100.0; + + return criteria; + } + catch (JsonException ex) + { + _logger.LogError(ex, "Failed to parse criteria JSON"); + return new ExtractedCriteria + { + RfpTitle = "Unknown RFP", + RfpSummary = "Failed to extract RFP summary", + ExtractionNotes = $"Error parsing response: {ex.Message}" + }; + } + } + + internal ProposalEvaluation ParseScoringResponse(string responseText, ExtractedCriteria criteria) + { + var text = CleanJsonResponse(responseText); + + try + { + using var doc = JsonDocument.Parse(text); + var root = doc.RootElement; + + var evaluation = new ProposalEvaluation + { + RfpTitle = root.TryGetProperty("rfp_title", out var rt) ? rt.GetString() ?? "" : criteria.RfpTitle, + SupplierName = root.TryGetProperty("supplier_name", out var sn) ? sn.GetString() ?? "Unknown Vendor" : "Unknown Vendor", + SupplierSite = root.TryGetProperty("supplier_site", out var ss) ? ss.GetString() ?? "" : "", + ResponseId = root.TryGetProperty("response_id", out var ri) ? ri.GetString() ?? "" : "", + EvaluationDate = root.TryGetProperty("evaluation_date", out var ed) ? ed.GetString() ?? DateTime.UtcNow.ToString("yyyy-MM-dd") : DateTime.UtcNow.ToString("yyyy-MM-dd"), + Recommendation = root.TryGetProperty("recommendation", out var rec) ? rec.GetString() ?? "" : "", + ExecutiveSummary = root.TryGetProperty("executive_summary", out var es) ? es.GetString() ?? "" : "", + RiskAssessment = root.TryGetProperty("risk_assessment", out var ra) ? ra.GetString() ?? "" : "" + }; + + if (root.TryGetProperty("criterion_scores", out var scoresArray)) + { + foreach (var cs in scoresArray.EnumerateArray()) + { + evaluation.CriterionScores.Add(new CriterionScore + { + CriterionId = cs.TryGetProperty("criterion_id", out var cid) ? cid.GetString() ?? "" : "", + CriterionName = cs.TryGetProperty("criterion_name", out var cn) ? cn.GetString() ?? "" : "", + Weight = cs.TryGetProperty("weight", out var w) ? w.GetDouble() : 0, + RawScore = cs.TryGetProperty("raw_score", out var rs) ? rs.GetDouble() : 0, + WeightedScore = cs.TryGetProperty("weighted_score", out var ws) ? ws.GetDouble() : 0, + Evidence = cs.TryGetProperty("evidence", out var ev) ? ev.GetString() ?? "" : "", + Justification = cs.TryGetProperty("justification", out var j) ? j.GetString() ?? "" : "", + Strengths = cs.TryGetProperty("strengths", out var str) ? str.EnumerateArray().Select(s => s.GetString() ?? "").ToList() : new(), + Gaps = cs.TryGetProperty("gaps", out var g) ? g.EnumerateArray().Select(s => s.GetString() ?? "").ToList() : new() + }); + } + } + + evaluation.OverallStrengths = ParseStringArray(root, "overall_strengths"); + evaluation.OverallWeaknesses = ParseStringArray(root, "overall_weaknesses"); + evaluation.Recommendations = ParseStringArray(root, "recommendations"); + + var totalScore = evaluation.CriterionScores.Sum(cs => cs.WeightedScore); + evaluation.TotalScore = Math.Round(totalScore, 2); + evaluation.ScorePercentage = Math.Round(totalScore, 2); + + evaluation.Grade = totalScore switch + { + >= 90 => "A", + >= 80 => "B", + >= 70 => "C", + >= 60 => "D", + _ => "F" + }; + + return evaluation; + } + catch (JsonException ex) + { + _logger.LogError(ex, "Failed to parse scoring JSON"); + return new ProposalEvaluation + { + RfpTitle = criteria.RfpTitle, + SupplierName = "Unknown Vendor", + EvaluationDate = DateTime.UtcNow.ToString("yyyy-MM-dd"), + Grade = "F", + Recommendation = "Unable to complete evaluation due to parsing error" + }; + } + } + + internal static string CleanJsonResponse(string text) + { + text = text.Trim(); + if (text.StartsWith("```json")) text = text[7..]; + else if (text.StartsWith("```")) text = text[3..]; + if (text.EndsWith("```")) text = text[..^3]; + return text.Trim(); + } + + private static List ParseStringArray(JsonElement root, string propertyName) + { + if (root.TryGetProperty(propertyName, out var array)) + { + return array.EnumerateArray().Select(s => s.GetString() ?? "").ToList(); + } + return new(); + } +} diff --git a/app/RfpAnalyzer/appsettings.Development.json b/app/RfpAnalyzer/appsettings.Development.json new file mode 100644 index 0000000..024e9a0 --- /dev/null +++ b/app/RfpAnalyzer/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.Agents": "Debug" + } + } +} diff --git a/app/RfpAnalyzer/appsettings.json b/app/RfpAnalyzer/appsettings.json new file mode 100644 index 0000000..40aad76 --- /dev/null +++ b/app/RfpAnalyzer/appsettings.json @@ -0,0 +1,16 @@ +{ + "AZURE_OPENAI_ENDPOINT": "https://.openai.azure.com/", + "AZURE_OPENAI_DEPLOYMENT_NAME": "", + "AZURE_CONTENT_UNDERSTANDING_ENDPOINT": "https://.cognitiveservices.azure.com/", + "AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT": "https://.cognitiveservices.azure.com/", + "AZURE_CLIENT_ID": "", + "APPLICATIONINSIGHTS_CONNECTION_STRING": "", + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + + "AllowedHosts": "*" +} diff --git a/app/RfpAnalyzer/appsettings.template.json b/app/RfpAnalyzer/appsettings.template.json new file mode 100644 index 0000000..c1b3680 --- /dev/null +++ b/app/RfpAnalyzer/appsettings.template.json @@ -0,0 +1,17 @@ +{ + "AZURE_OPENAI_ENDPOINT": "https://.openai.azure.com/", + "AZURE_OPENAI_DEPLOYMENT_NAME": "", + "AZURE_CONTENT_UNDERSTANDING_ENDPOINT": "https://.cognitiveservices.azure.com/", + "AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT": "https://.cognitiveservices.azure.com/", + "AZURE_CLIENT_ID": "", + "APPLICATIONINSIGHTS_CONNECTION_STRING": "", + + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + + "AllowedHosts": "*" +} diff --git a/app/RfpAnalyzer/wwwroot/app.css b/app/RfpAnalyzer/wwwroot/app.css new file mode 100644 index 0000000..43c1e11 --- /dev/null +++ b/app/RfpAnalyzer/wwwroot/app.css @@ -0,0 +1,15 @@ +/* MudBlazor handles most styling. Minimal overrides only. */ + +h1:focus { + outline: none; +} + +.blazor-error-boundary { + background: #b32121; + padding: 1rem; + color: white; +} + +.blazor-error-boundary::after { + content: "An error has occurred."; +} \ No newline at end of file diff --git a/app/RfpAnalyzer/wwwroot/favicon.png b/app/RfpAnalyzer/wwwroot/favicon.png new file mode 100644 index 0000000..8422b59 Binary files /dev/null and b/app/RfpAnalyzer/wwwroot/favicon.png differ diff --git a/app/docker-compose.yml b/app/docker-compose.yml deleted file mode 100644 index 9e554e9..0000000 --- a/app/docker-compose.yml +++ /dev/null @@ -1,64 +0,0 @@ -# ============================================================================= -# RFP Analyzer - Docker Compose Configuration -# ============================================================================= -# Usage: -# Development: docker compose up -# Production: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d -# Build: docker compose build -# -# Run from the app folder: cd app && docker compose up --build -# ============================================================================= - -services: - rfp-analyzer: - build: - context: . - dockerfile: Dockerfile - image: rfp-analyzer:latest - container_name: rfp-analyzer - ports: - - "8501:8501" - env_file: - - .env - environment: - # Azure AI Services / Content Understanding - - AZURE_CONTENT_UNDERSTANDING_ENDPOINT=${AZURE_CONTENT_UNDERSTANDING_ENDPOINT} - - AZURE_AI_API_KEY=${AZURE_AI_API_KEY:-} - - # Azure OpenAI - - AZURE_OPENAI_ENDPOINT=${AZURE_OPENAI_ENDPOINT} - - AZURE_OPENAI_DEPLOYMENT_NAME=${AZURE_OPENAI_DEPLOYMENT_NAME:-gpt-5.2} - - AZURE_OPENAI_API_KEY=${AZURE_OPENAI_API_KEY:-} - - # Azure Service Principal (optional - for non-interactive auth) - - AZURE_TENANT_ID=${AZURE_TENANT_ID:-} - - AZURE_CLIENT_ID=${AZURE_CLIENT_ID:-} - - AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET:-} - - # Resource limits - deploy: - resources: - limits: - memory: 2G - reservations: - memory: 512M - - # Restart policy - restart: unless-stopped - - # Health check - healthcheck: - test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8501/_stcore/health')"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - -# ============================================================================= -# Notes: -# ============================================================================= -# 1. Copy .env.example to .env and fill in your Azure credentials -# 2. For Azure AD authentication, set AZURE_TENANT_ID, AZURE_CLIENT_ID, -# and AZURE_CLIENT_SECRET for a service principal -# 3. Alternatively, use API keys: AZURE_AI_API_KEY and AZURE_OPENAI_API_KEY -# ============================================================================= diff --git a/app/global.json b/app/global.json new file mode 100644 index 0000000..512142d --- /dev/null +++ b/app/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + } +} diff --git a/app/main.py b/app/main.py deleted file mode 100644 index d84020e..0000000 --- a/app/main.py +++ /dev/null @@ -1,2669 +0,0 @@ -""" -RFP Analyzer - Streamlit Application - -A comprehensive workflow for analyzing RFPs and scoring vendor proposals: -1. Upload RFP file and Vendor proposals (multiple files) -2. Configure extraction service and evaluation criteria -3. AI-powered evaluation and multi-vendor comparison -""" - -import os -import streamlit as st -from pathlib import Path -import asyncio -import time -import logging -import io -import json -import uuid -from datetime import datetime -from typing import List, Dict, Any, Optional - -# Initialize centralized logging FIRST (before other imports) -from services.logging_config import setup_logging, get_logger -setup_logging() # Uses OTEL_LOGGING_ENABLED env var (default: False) - -from services.document_processor import DocumentProcessor, ExtractionService -from services.scoring_agent_v2 import ScoringAgentV2 -from services.comparison_agent import ComparisonAgent, generate_word_report, generate_full_analysis_report -from services.processing_queue import ProcessingQueue, QueueItem, QueueItemStatus - -# Optional PDF support -try: - from weasyprint import HTML, CSS - PDF_AVAILABLE = True -except ImportError: - PDF_AVAILABLE = False - -try: - import markdown - MARKDOWN_AVAILABLE = True -except ImportError: - MARKDOWN_AVAILABLE = False - -# Optional chart support -try: - import plotly.express as px - import plotly.graph_objects as go - PLOTLY_AVAILABLE = True -except ImportError: - PLOTLY_AVAILABLE = False - -# Get logger (logging is already configured at import time) -logger = get_logger(__name__) - -# Log application startup -logger.info("RFP Analyzer application starting...") -logger.info("Optional features - PDF: %s, Markdown: %s, Plotly: %s", - PDF_AVAILABLE, MARKDOWN_AVAILABLE, PLOTLY_AVAILABLE) - - -def format_duration(seconds: float) -> str: - """Format duration in minutes and seconds. - - Args: - seconds: Duration in seconds - - Returns: - Formatted string like "1m 30s" or "45s" for durations under a minute - """ - if seconds < 60: - return f"{seconds:.1f}s" - minutes = int(seconds // 60) - remaining_seconds = seconds % 60 - return f"{minutes}m {remaining_seconds:.1f}s" - - -def generate_pdf_from_markdown(markdown_content: str, title: str = "RFP Score Report") -> bytes | None: - """Generate PDF from markdown content. - - Args: - markdown_content: The markdown content to convert - title: Title for the PDF document - - Returns: - PDF bytes if successful, None if PDF generation is not available - """ - if not PDF_AVAILABLE or not MARKDOWN_AVAILABLE: - return None - - try: - # Convert markdown to HTML - html_content = markdown.markdown( - markdown_content, - extensions=['tables', 'fenced_code', 'toc'] - ) - - # CSS styling for the PDF - css = CSS(string=''' - @page { - size: A4; - margin: 2cm; - } - body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - font-size: 11pt; - line-height: 1.6; - color: #333; - } - h1 { - color: #1a1a1a; - border-bottom: 2px solid #0066cc; - padding-bottom: 10px; - font-size: 24pt; - } - h2 { - color: #0066cc; - margin-top: 20px; - font-size: 18pt; - } - h3 { - color: #333; - font-size: 14pt; - } - table { - width: 100%; - border-collapse: collapse; - margin: 15px 0; - font-size: 10pt; - } - th, td { - border: 1px solid #ddd; - padding: 8px; - text-align: left; - } - th { - background-color: #0066cc; - color: white; - } - tr:nth-child(even) { - background-color: #f9f9f9; - } - code { - background-color: #f4f4f4; - padding: 2px 6px; - border-radius: 3px; - font-family: "Courier New", monospace; - font-size: 10pt; - } - ul, ol { - margin-left: 20px; - } - li { - margin-bottom: 5px; - } - .score-excellent { color: #28a745; } - .score-good { color: #17a2b8; } - .score-average { color: #ffc107; } - .score-poor { color: #dc3545; } - ''') - - # Wrap HTML with proper structure - full_html = f''' - - - - - {title} - - - {html_content} - - - ''' - - # Generate PDF - pdf_buffer = io.BytesIO() - HTML(string=full_html).write_pdf(pdf_buffer, stylesheets=[css]) - pdf_buffer.seek(0) - return pdf_buffer.getvalue() - - except Exception as e: - logger.error(f"Failed to generate PDF: {e}") - return None - - -# Page configuration -st.set_page_config( - page_title="RFP Analyzer", - page_icon="๐Ÿ“„", - layout="wide", - initial_sidebar_state="expanded" -) - -# Initialize session state -if "step" not in st.session_state: - st.session_state.step = 0 # Start at landing page -if "rfp_file" not in st.session_state: - st.session_state.rfp_file = None -if "proposal_files" not in st.session_state: - st.session_state.proposal_files = [] # Changed to list for multiple proposals -if "rfp_content" not in st.session_state: - st.session_state.rfp_content = None -if "proposal_contents" not in st.session_state: - st.session_state.proposal_contents = {} # Dict mapping filename to content -if "evaluation_results" not in st.session_state: - st.session_state.evaluation_results = [] # List of evaluation results -if "comparison_results" not in st.session_state: - st.session_state.comparison_results = None -if "step_durations" not in st.session_state: - st.session_state.step_durations = {} -if "extraction_service" not in st.session_state: - st.session_state.extraction_service = ExtractionService.DOCUMENT_INTELLIGENCE -if "evaluation_mode" not in st.session_state: - st.session_state.evaluation_mode = "individual" # Always use individual mode -if "global_criteria" not in st.session_state: - st.session_state.global_criteria = "" -if "reasoning_effort" not in st.session_state: - st.session_state.reasoning_effort = "low" -# Queue states for tracking progress -if "extraction_queue" not in st.session_state: - st.session_state.extraction_queue = None -if "scoring_queue" not in st.session_state: - st.session_state.scoring_queue = None -if "is_processing" not in st.session_state: - st.session_state.is_processing = False - - -# Animation CSS for step indicators -STEP_ANIMATION_CSS = """ - -""" - - -def get_scoring_guide() -> str: - """Load the scoring guide from file.""" - guide_path = Path(__file__).parent / "scoring_guide.md" - if guide_path.exists(): - return guide_path.read_text(encoding="utf-8") - return "" - - -async def process_document( - file_bytes: bytes, - filename: str, - extraction_service: ExtractionService = ExtractionService.DOCUMENT_INTELLIGENCE -) -> tuple[str, float]: - """Process uploaded document using the configured extraction service. - - Args: - file_bytes: Document content as bytes - filename: Original filename - extraction_service: Which service to use for extraction - - Returns: - Tuple of (content, duration_seconds) - """ - start_time = time.time() - logger.info("Starting document processing: %s using %s", filename, extraction_service.value) - - processor = DocumentProcessor(service=extraction_service) - content = await processor.extract_content(file_bytes, filename) - - duration = time.time() - start_time - logger.info("Document processed: %s (%.2fs, %d chars)", filename, duration, len(content)) - - return content, duration - - -async def evaluate_proposal( - rfp_content: str, - proposal_content: str, - global_criteria: str = "", - reasoning_effort: str = "high", - progress_callback: callable = None -) -> tuple[dict, float]: - """Evaluate the vendor proposal against the RFP using AI agent. - - Args: - rfp_content: The RFP content - proposal_content: The proposal content - global_criteria: Optional user-provided global evaluation criteria - reasoning_effort: Reasoning effort level ("low", "medium", "high") - progress_callback: Optional callback for progress updates - - Returns: - Tuple of (results, duration_seconds) - """ - start_time = time.time() - logger.info("Starting proposal evaluation (effort: %s)...", reasoning_effort) - - # Always use V2 (multi-agent) for evaluation - agent = ScoringAgentV2() - - # Combine RFP content with global criteria if provided - if global_criteria: - enhanced_rfp = f"{rfp_content}\n\n## Additional Evaluation Criteria (User Specified)\n\n{global_criteria}" - else: - enhanced_rfp = rfp_content - - results = await agent.evaluate( - enhanced_rfp, - proposal_content, - reasoning_effort=reasoning_effort, - progress_callback=progress_callback - ) - - duration = time.time() - start_time - logger.info("Evaluation completed in %.2fs", duration) - - return results, duration - - -def render_sidebar(): - """Render the sidebar with configuration and navigation.""" - with st.sidebar: - st.title("๐Ÿ“„ RFP Analyzer") - st.markdown("---") - - # Only show configuration options when not on landing page - if st.session_state.step > 0: - # Extraction Service Selection - st.subheader("๐Ÿ”ง Document Extraction") - - service_options = { - ExtractionService.DOCUMENT_INTELLIGENCE: "Azure Document Intelligence", - ExtractionService.CONTENT_UNDERSTANDING: "Azure Content Understanding" - } - - service = st.radio( - "Extraction service:", - options=list(service_options.keys()), - index=0 if st.session_state.extraction_service == ExtractionService.DOCUMENT_INTELLIGENCE else 1, - format_func=lambda x: service_options[x], - help="Choose the Azure service for document text extraction.", - disabled=st.session_state.is_processing - ) - if service != st.session_state.extraction_service: - logger.info("Extraction service changed to: %s", service.value) - st.session_state.extraction_service = service - # Reset content when service changes - st.session_state.rfp_content = None - st.session_state.proposal_contents = {} - st.rerun() - - st.markdown("") - - # Analysis Depth Selection - st.subheader("๐Ÿง  Analysis Depth") - depth_options = { - "low": "Standard (~5 mins)", - "medium": "Thorough (~10 mins)", - "high": "Comprehensive (~15 mins)" - } - effort = st.radio( - "Analysis depth:", - options=["low", "medium", "high"], - index=["low", "medium", "high"].index(st.session_state.reasoning_effort), - format_func=lambda x: depth_options[x], - help="Higher depth = more detailed analysis but longer processing time.", - disabled=st.session_state.is_processing - ) - if effort != st.session_state.reasoning_effort: - logger.info("Analysis depth changed to: %s", effort) - st.session_state.reasoning_effort = effort - st.session_state.evaluation_results = [] - st.session_state.comparison_results = None - st.rerun() - - st.markdown("---") - - # Step indicators - st.subheader("๐Ÿ“ Progress") - steps = [ - ("1๏ธโƒฃ", "Upload Documents", st.session_state.step >= 1), - ("2๏ธโƒฃ", "Configure & Extract", st.session_state.step >= 2), - ("3๏ธโƒฃ", "Evaluate & Compare", st.session_state.step >= 3), - ] - - for icon, label, active in steps: - if active: - st.success(f"{icon} {label}") - else: - st.info(f"{icon} {label}") - - st.markdown("---") - - # Reset button - if st.button("๐Ÿ”„ Start Over", use_container_width=True, disabled=st.session_state.is_processing): - logger.info("User initiated application reset") - st.session_state.step = 0 - st.session_state.rfp_file = None - st.session_state.proposal_files = [] - st.session_state.rfp_content = None - st.session_state.proposal_contents = {} - st.session_state.evaluation_results = [] - st.session_state.comparison_results = None - st.session_state.global_criteria = "" - st.session_state.extraction_service = ExtractionService.DOCUMENT_INTELLIGENCE - st.session_state.evaluation_mode = "individual" - st.session_state.reasoning_effort = "low" - st.session_state.extraction_queue = None - st.session_state.scoring_queue = None - st.session_state.step_durations = {} - st.session_state.is_processing = False - st.rerun() - else: - # Landing page sidebar content - st.markdown("### ๐Ÿ‘‹ Welcome!") - st.markdown(""" - This tool helps you analyze RFP documents and evaluate vendor proposals using AI. - - **Features:** - - ๐Ÿ“„ Document extraction - - ๐ŸŽฏ AI-powered scoring - - ๐Ÿ“Š Multi-vendor comparison - - ๐Ÿ“ฅ Export reports - """) - - st.markdown("---") - st.caption("Powered by Azure AI Services, Microsoft Foundry & Agent Framework") - - -def render_landing_page(): - """Render the landing page with solution description and start button.""" - # Center content with columns - col1, col2, col3 = st.columns([1, 2, 1]) - - with col2: - st.markdown("") - st.markdown("") - - # Hero section - st.markdown(""" -
-

๐Ÿ“„ RFP Analyzer

-

AI-Powered RFP Analysis & Vendor Evaluation

-
- """, unsafe_allow_html=True) - - st.markdown("---") - - # Description - st.markdown(""" - ### Transform Your RFP Review Process - - RFP Analyzer uses advanced AI to streamline your vendor evaluation process. - Simply upload your RFP document and vendor proposals, and let our multi-agent - system do the heavy lifting. - """) - - # Feature cards - st.markdown("") - feature_col1, feature_col2 = st.columns(2) - - with feature_col1: - st.markdown(""" - #### ๐Ÿ” Smart Extraction - Automatically extract text from PDFs, Word documents, and more using Azure AI services. - - #### ๐ŸŽฏ Intelligent Scoring - AI agents analyze proposals against RFP criteria with detailed justifications. - """) - - with feature_col2: - st.markdown(""" - #### ๐Ÿ“Š Comparative Analysis - Compare multiple vendors side-by-side with visual dashboards and rankings. - - #### ๐Ÿ“ฅ Comprehensive Reports - Export detailed reports in Word, CSV, or JSON formats. - """) - - st.markdown("") - st.markdown("---") - - # How it works - st.markdown(""" - ### How It Works - - 1. **Upload Documents** โ€” Add your RFP and vendor proposals - 2. **Configure & Extract** โ€” Select settings and extract document content - 3. **Evaluate & Compare** โ€” AI analyzes and scores each proposal - 4. **Review & Export** โ€” View results and download reports - """) - - st.markdown("") - st.markdown("") - - # Start button - if st.button( - "๐Ÿš€ Start Analysis", - type="primary", - use_container_width=True, - disabled=st.session_state.is_processing, - help="Click to begin the RFP analysis process" - ): - logger.info("User started RFP analysis from landing page") - st.session_state.step = 1 - st.rerun() - - st.markdown("") - st.caption("Tip: Have your RFP document and vendor proposals ready before starting.") - - -def render_step1(): - """Step 1: Upload RFP and Vendor Proposals.""" - st.header("Step 1: Upload Documents") - st.markdown("Upload the RFP document and vendor proposal files to begin the analysis.") - - col1, col2 = st.columns(2) - - with col1: - st.subheader("๐Ÿ“„ RFP Document") - st.markdown("Upload a single RFP file") - - rfp_file = st.file_uploader( - "Choose RFP file", - type=["pdf", "docx", "doc", "txt", "md"], - key="rfp_uploader", - help="Supported formats: PDF, Word documents, Text files, Markdown" - ) - - if rfp_file is not None: - st.info(f"๐Ÿ“Ž **{rfp_file.name}** ({rfp_file.size / 1024:.1f} KB)") - if st.session_state.rfp_file is None or st.session_state.rfp_file.get('name') != rfp_file.name: - logger.info("RFP file uploaded: %s (%.1f KB)", rfp_file.name, rfp_file.size / 1024) - st.session_state.rfp_file = { - "bytes": rfp_file.getvalue(), - "name": rfp_file.name - } - - if st.session_state.rfp_file: - st.success(f"โœ… RFP ready: {st.session_state.rfp_file['name']}") - - with col2: - st.subheader("๐Ÿ“ Vendor Proposals") - st.markdown("Upload one or more vendor proposal files") - - proposal_files = st.file_uploader( - "Choose Vendor Proposal files", - type=["pdf", "docx", "doc", "txt", "md"], - key="proposals_uploader", - accept_multiple_files=True, - help="Supported formats: PDF, Word documents, Text files, Markdown" - ) - - if proposal_files: - st.info(f"๐Ÿ“Ž {len(proposal_files)} file(s) selected") - for f in proposal_files: - st.caption(f"โ€ข {f.name} ({f.size / 1024:.1f} KB)") - - # Log new uploads - current_names = {p.get('name') for p in st.session_state.proposal_files} if st.session_state.proposal_files else set() - new_names = {f.name for f in proposal_files} - if current_names != new_names: - logger.info("Proposal files uploaded: %d files - %s", - len(proposal_files), - ", ".join(f.name for f in proposal_files)) - - st.session_state.proposal_files = [ - {"bytes": f.getvalue(), "name": f.name} - for f in proposal_files - ] - - if st.session_state.proposal_files: - st.success(f"โœ… {len(st.session_state.proposal_files)} proposal(s) ready") - - st.markdown("---") - - # Global Evaluation Criteria - st.subheader("๐Ÿ“‹ Global Evaluation Criteria (Optional)") - st.markdown("Add additional evaluation criteria that will be used alongside criteria extracted from the RFP.") - - global_criteria = st.text_area( - "Enter additional evaluation criteria:", - value=st.session_state.global_criteria, - height=150, - placeholder="Example:\n- Cost effectiveness: 20%\n- Sustainability practices: 15%\n- Local presence: 10%", - help="These criteria will be added to the automatically extracted criteria from the RFP." - ) - st.session_state.global_criteria = global_criteria - - st.markdown("---") - - # Proceed button - can_proceed = st.session_state.rfp_file is not None and len(st.session_state.proposal_files) > 0 - - if can_proceed: - if st.button( - "Continue to Step 2: Extract Content โ†’", - type="primary", - use_container_width=True, - disabled=st.session_state.is_processing - ): - logger.info("User proceeding to Step 2 - RFP: %s, Proposals: %d", - st.session_state.rfp_file['name'], - len(st.session_state.proposal_files)) - st.session_state.step = 2 - st.rerun() - else: - st.info("๐Ÿ“Œ Please upload an RFP file and at least one vendor proposal to continue.") - - -def render_step2(): - """Step 2: Extract Content from Documents.""" - st.header("Step 2: Extract & Configure") - st.markdown("Extract content from uploaded documents and configure evaluation settings.") - - # Show uploaded files summary - col1, col2 = st.columns(2) - with col1: - if st.session_state.rfp_file: - st.info(f"๐Ÿ“„ RFP: {st.session_state.rfp_file['name']}") - with col2: - if st.session_state.proposal_files: - st.info(f"๐Ÿ“ Proposals: {len(st.session_state.proposal_files)} file(s)") - - # Show configuration summary - st.markdown("---") - st.subheader("โš™๏ธ Current Configuration") - - config_col1, config_col2 = st.columns(2) - with config_col1: - service_name = "Content Understanding" if st.session_state.extraction_service == ExtractionService.CONTENT_UNDERSTANDING else "Document Intelligence" - st.metric("Extraction Service", service_name) - with config_col2: - st.metric("Analysis Depth", st.session_state.reasoning_effort.title()) - - # Global criteria preview - if st.session_state.global_criteria: - with st.expander("๐Ÿ“‹ Global Evaluation Criteria", expanded=False): - st.markdown(st.session_state.global_criteria) - - st.markdown("---") - - # Check if content has been extracted - if st.session_state.rfp_content and st.session_state.proposal_contents: - st.success("โœ… All documents have been processed!") - - # Show extraction queue summary if available - if st.session_state.extraction_queue: - queue = st.session_state.extraction_queue - with st.expander("๐Ÿ“Š Extraction Summary", expanded=False): - col1, col2, col3 = st.columns(3) - with col1: - st.metric("Documents", len(queue.items)) - with col2: - st.metric("Total Time", format_duration(queue.get_total_duration())) - with col3: - st.metric("Avg per Doc", format_duration(queue.get_average_item_duration())) - - for item in queue.items: - if item.duration: - st.markdown(f"โ€ข **{item.name}**: `{format_duration(item.duration)}`") - - # Show extracted content previews - with st.expander("๐Ÿ“„ RFP Content Preview", expanded=False): - st.markdown(st.session_state.rfp_content[:2000] + "..." if len(st.session_state.rfp_content) > 2000 else st.session_state.rfp_content) - - with st.expander("๐Ÿ“ Proposal Contents Preview", expanded=False): - for filename, content in st.session_state.proposal_contents.items(): - st.markdown(f"### {filename}") - st.markdown(content[:1000] + "..." if len(content) > 1000 else content) - st.markdown("---") - - if st.button( - "Continue to Step 3: Evaluate โ†’", - type="primary", - use_container_width=True, - disabled=st.session_state.is_processing - ): - logger.info("User proceeding to Step 3 - Evaluation") - st.session_state.step = 3 - st.rerun() - else: - # Run extraction - if st.button( - "๐Ÿ” Extract Document Content", - type="primary", - use_container_width=True, - disabled=st.session_state.is_processing - ): - logger.info("User starting document extraction") - st.session_state.is_processing = True - run_extraction_pipeline() - - # Navigation - st.markdown("---") - if st.button("โฌ…๏ธ Back to Step 1", disabled=st.session_state.is_processing): - logger.info("User navigating back to Step 1") - st.session_state.step = 1 - st.rerun() - - -def run_extraction_pipeline(): - """Run the document extraction pipeline with parallel processing and live progress.""" - extraction_service = st.session_state.extraction_service - logger.info("====== EXTRACTION PIPELINE STARTED (Service: %s) ======", extraction_service.value) - pipeline_start = time.time() - - # Inject animation CSS - st.markdown(STEP_ANIMATION_CSS, unsafe_allow_html=True) - - # Create extraction queue - extraction_queue = ProcessingQueue(name="Document Extraction") - - # Add RFP to queue with unique request ID - rfp_file = st.session_state.rfp_file - rfp_request_id = str(uuid.uuid4())[:8] - extraction_queue.add_item( - id="rfp", - name=rfp_file['name'], - item_type="rfp", - metadata={ - "filename": rfp_file["name"], - "size": len(rfp_file["bytes"]), - "request_id": rfp_request_id - } - ) - logger.info("Queued RFP for extraction: %s (request_id: %s)", rfp_file['name'], rfp_request_id) - - # Add proposals to queue with unique request IDs - for i, proposal_file in enumerate(st.session_state.proposal_files): - proposal_request_id = str(uuid.uuid4())[:8] - extraction_queue.add_item( - id=f"proposal_{i}", - name=proposal_file['name'], - item_type="proposal", - metadata={ - "filename": proposal_file["name"], - "size": len(proposal_file["bytes"]), - "request_id": proposal_request_id - } - ) - logger.info("Queued proposal for extraction: %s (request_id: %s)", - proposal_file['name'], proposal_request_id) - - extraction_queue.start() - st.session_state.extraction_queue = extraction_queue - - # UI Setup - single placeholder for live updates - st.subheader("๐Ÿ“„ Extracting Documents") - - # Create a single placeholder that we'll update - status_placeholder = st.empty() - - try: - all_files = [rfp_file] + st.session_state.proposal_files - total_files = len(all_files) - - # Mark all items as processing - for item in extraction_queue.items: - item.start() - - # Define async function for parallel processing - async def process_all_documents(): - tasks = [] - for file_data in all_files: - task = process_document( - file_data["bytes"], - file_data["name"], - extraction_service - ) - tasks.append(task) - return await asyncio.gather(*tasks, return_exceptions=True) - - # Start timer update loop in a separate display - def render_status(): - """Render the current queue status.""" - with status_placeholder.container(): - # Overall progress - elapsed = time.time() - pipeline_start - st.markdown(f"**โฑ๏ธ Elapsed: `{format_duration(elapsed)}`** | Processing {total_files} documents in parallel...") - - # Progress bar - progress = extraction_queue.get_progress() - st.progress(progress["percentage"] / 100) - - # Items status - for item in extraction_queue.items: - icon = item.get_status_icon() - elapsed_time = format_duration(item.get_elapsed_time()) if item.start_time else "-" - - if item.item_type == "rfp": - label = f"๐Ÿ“„ RFP: {item.name}" - else: - label = f"๐Ÿ“ Proposal: {item.name}" - - if item.status == QueueItemStatus.COMPLETED: - st.success(f"{icon} {label} โ€” `{elapsed_time}`") - elif item.status == QueueItemStatus.PROCESSING: - st.info(f"{icon} {label} โ€” Processing... `{elapsed_time}`") - elif item.status == QueueItemStatus.FAILED: - st.error(f"{icon} {label} โ€” Failed") - else: - st.warning(f"{icon} {label} โ€” Waiting...") - - # Initial render - render_status() - - # Run parallel processing - results = asyncio.run(process_all_documents()) - - # Process results and update queue - extracted_results = [] - for i, result in enumerate(results): - item_id = "rfp" if i == 0 else f"proposal_{i-1}" - item = extraction_queue.get_item(item_id) - - if isinstance(result, Exception): - item.fail(str(result)) - logger.error("Failed to extract document %d: %s", i, str(result)) - extracted_results.append((None, 0)) - else: - content, duration = result - item.complete(result=content) - extracted_results.append((content, duration)) - logger.info("Extracted document %d in %.2fs", i, duration) - - # Final render - render_status() - - # Check for failures - failed_items = extraction_queue.get_failed_items() - if failed_items: - raise Exception(f"Failed to extract {len(failed_items)} document(s)") - - # Store results - rfp_content, rfp_duration = extracted_results[0] - st.session_state.rfp_content = rfp_content - st.session_state.step_durations["rfp_processing"] = rfp_duration - - proposal_contents = {} - for i, file_data in enumerate(st.session_state.proposal_files): - content, duration = extracted_results[i + 1] - proposal_contents[file_data["name"]] = content - st.session_state.step_durations[f"proposal_{i}_processing"] = duration - - st.session_state.proposal_contents = proposal_contents - - extraction_queue.finish() - st.session_state.extraction_queue = extraction_queue - - total_duration = time.time() - pipeline_start - st.session_state.step_durations["extraction_total"] = total_duration - - logger.info("====== EXTRACTION PIPELINE COMPLETED in %.2fs ======", total_duration) - - # Show completion message - st.success(f"โœ… **All {total_files} documents extracted in {format_duration(total_duration)}!**") - - st.session_state.is_processing = False - time.sleep(1) - st.rerun() - - except Exception as e: - logger.error("Extraction pipeline failed: %s", str(e)) - extraction_queue.finish() - st.session_state.extraction_queue = extraction_queue - st.session_state.is_processing = False - st.error(f"โŒ Error during extraction: {str(e)}") - - -def render_step3(): - """Step 3: Evaluate and Compare.""" - st.header("Step 3: Evaluate & Compare") - st.markdown("Evaluating vendor proposals against RFP requirements and generating comparison.") - - # Show files summary - col1, col2 = st.columns(2) - with col1: - if st.session_state.rfp_file: - st.info(f"๐Ÿ“„ RFP: {st.session_state.rfp_file['name']}") - with col2: - if st.session_state.proposal_files: - st.info(f"๐Ÿ“ Proposals: {len(st.session_state.proposal_files)} file(s)") - - # Show evaluation process info - with st.expander("๐Ÿค– Multi-Agent Evaluation Process", expanded=False): - st.info(""" - **Evaluation Process:** - - 1. **Criteria Extraction Agent**: Analyzes the RFP document to automatically - extract scoring criteria with weights (totaling 100%). - - 2. **Proposal Scoring Agent**: Evaluates each vendor proposal against the - extracted criteria, providing detailed scores and justifications. - - 3. **Comparison Agent**: Compares all vendor scores and generates a - comprehensive comparison report with rankings. - - This approach ensures that scoring is tailored to each specific RFP's requirements. - """) - - # Check if evaluation has been completed - if st.session_state.evaluation_results and st.session_state.comparison_results: - render_comparison_results() - else: - # Start evaluation - if st.button( - "๐ŸŽฏ Start Evaluation", - type="primary", - use_container_width=True, - disabled=st.session_state.is_processing - ): - logger.info("User starting evaluation - Mode: individual, Effort: %s, Proposals: %d", - st.session_state.reasoning_effort, - len(st.session_state.proposal_files)) - st.session_state.is_processing = True - run_evaluation_pipeline() - - # Navigation - st.markdown("---") - if st.button("โฌ…๏ธ Back to Step 2", disabled=st.session_state.is_processing): - logger.info("User navigating back to Step 2") - st.session_state.step = 2 - st.session_state.evaluation_results = [] - st.session_state.comparison_results = None - st.rerun() - - -def run_evaluation_pipeline(): - """Run the full multi-vendor evaluation pipeline with parallel processing and clean UI.""" - reasoning_effort = st.session_state.reasoning_effort - global_criteria = st.session_state.global_criteria - - logger.info("====== EVALUATION PIPELINE STARTED (Effort: %s) ======", reasoning_effort) - pipeline_start = time.time() - - # Inject animation CSS - st.markdown(STEP_ANIMATION_CSS, unsafe_allow_html=True) - - # Create scoring queue - scoring_queue = ProcessingQueue(name="Proposal Scoring") - proposal_files = st.session_state.proposal_files - - # Add each proposal as a separate queue item with unique request ID - for i, proposal_file in enumerate(proposal_files): - request_id = str(uuid.uuid4())[:8] # Short unique ID - scoring_queue.add_item( - id=f"proposal_{i}", - name=proposal_file['name'], - item_type="evaluation", - metadata={ - "filename": proposal_file["name"], - "request_id": request_id - } - ) - logger.info("Queued proposal for scoring: %s (request_id: %s)", - proposal_file['name'], request_id) - - # Add comparison step if multiple proposals - if len(proposal_files) > 1: - comparison_request_id = str(uuid.uuid4())[:8] - scoring_queue.add_item( - id="comparison", - name="Vendor Comparison", - item_type="comparison", - metadata={"request_id": comparison_request_id} - ) - logger.info("Queued vendor comparison (request_id: %s)", comparison_request_id) - - scoring_queue.start() - st.session_state.scoring_queue = scoring_queue - - # UI Setup - single placeholder for clean updates - st.subheader("๐ŸŽฏ Evaluating Proposals") - status_placeholder = st.empty() - - def render_status(): - """Render the current scoring status.""" - with status_placeholder.container(): - elapsed = time.time() - pipeline_start - st.markdown(f"**โฑ๏ธ Elapsed: `{format_duration(elapsed)}`**") - - # Progress bar - progress = scoring_queue.get_progress() - st.progress(progress["percentage"] / 100) - - # Items status - for item in scoring_queue.items: - icon = item.get_status_icon() - elapsed_time = format_duration(item.get_elapsed_time()) if item.start_time else "-" - - if item.item_type == "comparison": - label = f"๐Ÿ“Š {item.name}" - else: - label = f"๐Ÿ“ {item.name}" - - if item.status == QueueItemStatus.COMPLETED: - extra = "" - if item.result and isinstance(item.result, dict) and "total_score" in item.result: - score = item.result["total_score"] - grade = item.result.get("grade", "") - extra = f" โ€” Score: **{score:.1f}** ({grade})" - st.success(f"{icon} {label}{extra} โ€” `{elapsed_time}`") - elif item.status == QueueItemStatus.PROCESSING: - st.info(f"{icon} {label} โ€” Processing... `{elapsed_time}`") - elif item.status == QueueItemStatus.FAILED: - st.error(f"{icon} {label} โ€” Failed: {item.error_message}") - else: - st.warning(f"{icon} {label} โ€” Waiting...") - - try: - evaluation_results = [] - - # Mark all proposal items as processing - for i in range(len(proposal_files)): - item = scoring_queue.get_item(f"proposal_{i}") - item.start() - - render_status() - - # Define async function for parallel proposal evaluation - async def evaluate_all_proposals(): - tasks = [] - for i, proposal_file in enumerate(proposal_files): - proposal_name = proposal_file["name"] - proposal_content = st.session_state.proposal_contents.get(proposal_name, "") - - task = evaluate_proposal( - st.session_state.rfp_content, - proposal_content, - global_criteria=global_criteria, - reasoning_effort=reasoning_effort - ) - tasks.append(task) - return await asyncio.gather(*tasks, return_exceptions=True) - - # Run parallel evaluation - results = asyncio.run(evaluate_all_proposals()) - - # Process results - for i, result in enumerate(results): - proposal_name = proposal_files[i]["name"] - item = scoring_queue.get_item(f"proposal_{i}") - - if isinstance(result, Exception): - item.fail(str(result)) - logger.error("Failed to evaluate %s: %s", proposal_name, str(result)) - else: - eval_result, duration = result - eval_result["_proposal_file"] = proposal_name - evaluation_results.append(eval_result) - item.complete(result=eval_result) - st.session_state.step_durations[f"eval_{proposal_name}"] = duration - logger.info("Proposal %s evaluated in %.2fs", proposal_name, duration) - - render_status() - - # Check for failures using get_failed_items - failed_items = scoring_queue.get_failed_items() - if failed_items: - raise Exception(f"Failed to evaluate {len(failed_items)} proposal(s)") - - st.session_state.evaluation_results = evaluation_results - - # Compare results if multiple proposals - if len(evaluation_results) > 1: - comparison_item = scoring_queue.get_item("comparison") - comparison_item.start() - - render_status() - - try: - comparison_agent = ComparisonAgent() - rfp_title = evaluation_results[0].get("rfp_title", "RFP Evaluation") - - comparison_results = asyncio.run( - comparison_agent.compare_evaluations( - evaluation_results, - rfp_title, - reasoning_effort=reasoning_effort - ) - ) - - st.session_state.comparison_results = comparison_results - comparison_item.complete(result=comparison_results) - logger.info("Comparison completed") - - except Exception as e: - comparison_item.fail(str(e)) - raise - else: - # Single proposal or combined mode - create basic comparison structure - if evaluation_results: - st.session_state.comparison_results = { - "rfp_title": evaluation_results[0].get("rfp_title", "RFP Evaluation"), - "total_vendors": len(evaluation_results), - "vendor_rankings": [{ - "rank": 1, - "vendor_name": evaluation_results[0].get("supplier_name", "Unknown"), - "total_score": evaluation_results[0].get("total_score", 0), - "grade": evaluation_results[0].get("grade", "N/A"), - "key_strengths": evaluation_results[0].get("overall_strengths", [])[:3], - "key_concerns": evaluation_results[0].get("overall_weaknesses", [])[:3], - "recommendation": evaluation_results[0].get("recommendation", "") - }], - "selection_recommendation": evaluation_results[0].get("recommendation", ""), - "comparison_insights": [] - } - - scoring_queue.finish() - st.session_state.scoring_queue = scoring_queue - - total_duration = time.time() - pipeline_start - st.session_state.step_durations["evaluation_total"] = total_duration - - logger.info("====== EVALUATION PIPELINE COMPLETED in %.2fs ======", total_duration) - - # Final render - render_status() - - st.success(f"โœ… **Evaluation complete in {format_duration(total_duration)}!**") - - st.session_state.is_processing = False - time.sleep(1) - st.rerun() - - except Exception as e: - logger.error("Evaluation pipeline failed: %s", str(e)) - scoring_queue.finish() - st.session_state.scoring_queue = scoring_queue - st.session_state.is_processing = False - render_status() - st.error(f"โŒ Error during evaluation: {str(e)}") - - -def render_comparison_results(): - """Render the multi-vendor comparison results.""" - st.markdown("---") - - comparison = st.session_state.comparison_results - evaluations = st.session_state.evaluation_results - - # Display timing if available - if st.session_state.step_durations: - total_time = st.session_state.step_durations.get("evaluation_total", 0) - st.info(f"โฑ๏ธ Total evaluation time: {format_duration(total_time)}") - - # Vendor Rankings Summary - st.subheader("๐Ÿ† Vendor Rankings") - - rankings = comparison.get("vendor_rankings", []) - if rankings: - cols = st.columns(min(len(rankings), 4)) - for i, ranking in enumerate(rankings[:4]): - with cols[i]: - rank = ranking.get("rank", i+1) - medal = "๐Ÿฅ‡" if rank == 1 else "๐Ÿฅˆ" if rank == 2 else "๐Ÿฅ‰" if rank == 3 else "๐Ÿ…" - st.metric( - label=f"{medal} #{rank}", - value=ranking.get("vendor_name", "Unknown")[:20], - delta=f"{ranking.get('total_score', 0):.1f} ({ranking.get('grade', 'N/A')})" - ) - - # Selection Recommendation - recommendation = comparison.get("selection_recommendation", "") - if recommendation: - st.success(f"โœ… **Recommendation:** {recommendation}") - - st.markdown("---") - - # Tabs for different views - tab1, tab2, tab3, tab4, tab5 = st.tabs([ - "๐Ÿ“Š Dashboard", - "๐Ÿ” Comparison Overview", - "๐Ÿ“‹ Individual Reports", - "๐Ÿ“ˆ Detailed Scores", - "๐Ÿ“ฅ Export" - ]) - - with tab1: - render_metrics_dashboard(comparison, evaluations) - - with tab2: - render_comparison_overview(comparison, evaluations) - - with tab3: - render_individual_reports(evaluations) - - with tab4: - render_detailed_scores(evaluations) - - with tab5: - render_export_options(comparison, evaluations) - - -def render_metrics_dashboard(comparison: dict, evaluations: list): - """Render the metrics dashboard with pie charts for each criterion/competency.""" - st.subheader("๐Ÿ“Š Metrics Dashboard") - st.markdown("Visual comparison of vendor performance across all evaluation criteria.") - - if not evaluations: - st.warning("No evaluations available to display.") - return - - if not PLOTLY_AVAILABLE: - st.warning("๐Ÿ“Š Plotly is not installed. Install it with `pip install plotly` for interactive charts.") - # Fallback to basic metrics display - _render_basic_metrics_dashboard(comparison, evaluations) - return - - # Get all criteria from first evaluation - criteria = [] - if evaluations[0].get("criterion_scores"): - criteria = evaluations[0]["criterion_scores"] - - if not criteria: - st.warning("No criteria scores available.") - return - - # Overall vendor comparison bar chart (total scores) - st.markdown("### ๐Ÿ† Overall Vendor Performance") - _render_overall_comparison_bar(evaluations) - - st.markdown("---") - st.markdown("### ๐Ÿ“ˆ Performance by Criterion") - st.markdown("Each chart shows how vendor scores compare for a specific evaluation criterion. " - "Taller bars indicate higher scores.") - - # Create bar charts for each criterion - num_criteria = len(criteria) - cols_per_row = 2 - - for i in range(0, num_criteria, cols_per_row): - cols = st.columns(cols_per_row) - - for j in range(cols_per_row): - criterion_idx = i + j - if criterion_idx >= num_criteria: - break - - criterion = criteria[criterion_idx] - criterion_name = criterion.get("criterion_name", f"Criterion {criterion_idx + 1}") - criterion_weight = criterion.get("weight", 0) - - with cols[j]: - _render_criterion_bar_chart(evaluations, criterion_idx, criterion_name, criterion_weight) - - # Vendor recommendation section - st.markdown("---") - st.markdown("### ๐Ÿ’ก Vendor Recommendations by Criterion") - _render_criterion_recommendations(comparison, evaluations) - - -def _render_overall_comparison_bar(evaluations: list): - """Render bar chart showing overall vendor comparison.""" - vendor_names = [] - total_scores = [] - grades = [] - - for eval_result in evaluations: - vendor_name = eval_result.get("supplier_name", "Unknown") - total_score = eval_result.get("total_score", 0) - grade = eval_result.get("grade", "N/A") - vendor_names.append(vendor_name) - total_scores.append(total_score) - grades.append(grade) - - # Sort by score descending - sorted_data = sorted(zip(vendor_names, total_scores, grades), key=lambda x: x[1], reverse=True) - vendor_names, total_scores, grades = zip(*sorted_data) if sorted_data else ([], [], []) - - # Create bar chart - fig = px.bar( - x=list(vendor_names), - y=list(total_scores), - title="Total Score Comparison", - labels={"x": "Vendor", "y": "Total Score"}, - color=list(total_scores), - color_continuous_scale="RdYlGn", - text=[f"{s:.1f} ({g})" for s, g in zip(total_scores, grades)] - ) - - fig.update_traces( - textposition='outside', - hovertemplate="%{x}
Score: %{y:.1f}" - ) - - fig.update_layout( - showlegend=False, - xaxis_title="Vendor", - yaxis_title="Total Score", - yaxis_range=[0, 105], - margin=dict(t=50, b=50, l=50, r=20), - height=400, - coloraxis_showscale=False - ) - - st.plotly_chart(fig, use_container_width=True) - - -def _render_criterion_bar_chart(evaluations: list, criterion_idx: int, criterion_name: str, weight: float): - """Render a bar chart for a specific criterion showing vendor scores.""" - vendor_names = [] - scores = [] - - for eval_result in evaluations: - vendor_name = eval_result.get("supplier_name", "Unknown") - criterion_scores = eval_result.get("criterion_scores", []) - - if criterion_idx < len(criterion_scores): - score = criterion_scores[criterion_idx].get("raw_score", 0) - else: - score = 0 - - vendor_names.append(vendor_name) - scores.append(score) - - # Sort by score descending - sorted_data = sorted(zip(vendor_names, scores), key=lambda x: x[1], reverse=True) - vendor_names, scores = zip(*sorted_data) if sorted_data else ([], []) - - # Find best vendor for this criterion - best_vendor = vendor_names[0] if vendor_names else "N/A" - best_score = scores[0] if scores else 0 - - # Create bar chart - fig = px.bar( - x=list(vendor_names), - y=list(scores), - title=f"{criterion_name}
Weight: {weight:.1f}% | Best: {best_vendor} ({best_score:.1f})", - labels={"x": "Vendor", "y": "Score"}, - color=list(scores), - color_continuous_scale="RdYlGn", - text=[f"{s:.1f}" for s in scores] - ) - - fig.update_traces( - textposition='outside', - hovertemplate="%{x}
Score: %{y:.1f}/100" - ) - - fig.update_layout( - showlegend=False, - xaxis_title="", - yaxis_title="Score", - yaxis_range=[0, 105], - margin=dict(t=60, b=30, l=40, r=10), - height=300, - coloraxis_showscale=False, - xaxis_tickangle=-45 if len(vendor_names) > 3 else 0 - ) - - st.plotly_chart(fig, use_container_width=True, key=f"bar_{criterion_idx}") - - -def _render_criterion_recommendations(comparison: dict, evaluations: list): - """Render recommendations for each criterion based on vendor performance.""" - criterion_comparisons = comparison.get("criterion_comparisons", []) - - if not criterion_comparisons: - # Fall back to generating recommendations from evaluations - if not evaluations or not evaluations[0].get("criterion_scores"): - return - - criteria = evaluations[0]["criterion_scores"] - for criterion_idx, criterion in enumerate(criteria): - criterion_name = criterion.get("criterion_name", f"Criterion {criterion_idx + 1}") - - # Find best vendor for this criterion - best_vendor = None - best_score = -1 - all_scores = [] - - for eval_result in evaluations: - vendor_name = eval_result.get("supplier_name", "Unknown") - criterion_scores = eval_result.get("criterion_scores", []) - - if criterion_idx < len(criterion_scores): - score = criterion_scores[criterion_idx].get("raw_score", 0) - all_scores.append((vendor_name, score)) - if score > best_score: - best_score = score - best_vendor = vendor_name - - if best_vendor: - with st.expander(f"**{criterion_name}** - Recommended: {best_vendor}"): - st.markdown(f"**Best Performer:** {best_vendor} (Score: {best_score:.1f}/100)") - - # Show all vendor scores - st.markdown("**All Vendors:**") - for vendor, score in sorted(all_scores, key=lambda x: x[1], reverse=True): - icon = "๐Ÿฅ‡" if vendor == best_vendor else "๐Ÿ“Š" - st.markdown(f" {icon} {vendor}: {score:.1f}/100") - else: - # Use the comparison agent's criterion comparisons - for cc in criterion_comparisons: - criterion_name = cc.get("criterion_name", "Unknown") - best_vendor = cc.get("best_vendor", "N/A") - worst_vendor = cc.get("worst_vendor", "N/A") - score_range = cc.get("score_range", "N/A") - insights = cc.get("insights", "") - weight = cc.get("weight", 0) - - with st.expander(f"**{criterion_name}** (Weight: {weight:.1f}%) - Recommended: {best_vendor}"): - col1, col2, col3 = st.columns(3) - with col1: - st.metric("๐Ÿฅ‡ Best", best_vendor) - with col2: - st.metric("๐Ÿ“‰ Lowest", worst_vendor) - with col3: - st.metric("๐Ÿ“Š Score Range", score_range) - - if insights: - st.markdown(f"**Why choose {best_vendor}:** {insights}") - - -def _render_basic_metrics_dashboard(comparison: dict, evaluations: list): - """Render a basic metrics dashboard without plotly charts.""" - st.markdown("### Vendor Performance Summary") - - # Overall comparison table - st.markdown("#### Total Scores") - for eval_result in sorted(evaluations, key=lambda x: x.get("total_score", 0), reverse=True): - vendor_name = eval_result.get("supplier_name", "Unknown") - total_score = eval_result.get("total_score", 0) - grade = eval_result.get("grade", "N/A") - - # Progress bar visualization - st.markdown(f"**{vendor_name}**: {total_score:.1f}/100 ({grade})") - st.progress(min(total_score / 100, 1.0)) - - st.markdown("---") - st.markdown("#### Criterion Scores") - - if evaluations and evaluations[0].get("criterion_scores"): - criteria = evaluations[0]["criterion_scores"] - - for criterion_idx, criterion in enumerate(criteria): - criterion_name = criterion.get("criterion_name", f"Criterion {criterion_idx + 1}") - criterion_weight = criterion.get("weight", 0) - - st.markdown(f"**{criterion_name}** (Weight: {criterion_weight:.1f}%)") - - for eval_result in evaluations: - vendor_name = eval_result.get("supplier_name", "Unknown") - criterion_scores = eval_result.get("criterion_scores", []) - - if criterion_idx < len(criterion_scores): - score = criterion_scores[criterion_idx].get("raw_score", 0) - col1, col2 = st.columns([3, 1]) - with col1: - st.progress(min(score / 100, 1.0)) - with col2: - st.write(f"{vendor_name[:15]}: {score:.1f}") - - -def render_comparison_overview(comparison: dict, evaluations: list): - """Render the comparison overview tab.""" - st.subheader("๐Ÿ“Š Multi-Vendor Comparison") - - # Comparison insights - insights = comparison.get("comparison_insights", []) - if insights: - st.markdown("### Key Insights") - for insight in insights: - st.markdown(f"โ€ข {insight}") - - # Winner summary - winner_summary = comparison.get("winner_summary", "") - if winner_summary: - st.markdown("### Winner Summary") - st.info(winner_summary) - - # Risk comparison - risk_comparison = comparison.get("risk_comparison", "") - if risk_comparison: - st.markdown("### Risk Assessment") - st.warning(risk_comparison) - - # Criterion comparisons - criterion_comparisons = comparison.get("criterion_comparisons", []) - if criterion_comparisons: - st.markdown("### Performance by Criterion") - - for cc in criterion_comparisons: - with st.expander(f"**{cc.get('criterion_name', 'Unknown')}** (Weight: {cc.get('weight', 0):.1f}%)"): - col1, col2 = st.columns(2) - with col1: - st.metric("Best Performer", cc.get("best_vendor", "N/A")) - with col2: - st.metric("Score Range", cc.get("score_range", "N/A")) - st.caption(cc.get("insights", "")) - - -def render_individual_reports(evaluations: list): - """Render individual vendor reports.""" - st.subheader("๐Ÿ“‹ Individual Vendor Reports") - - for i, eval_result in enumerate(evaluations): - vendor_name = eval_result.get("supplier_name", f"Vendor {i+1}") - proposal_file = eval_result.get("_proposal_file", "Unknown") - total_score = eval_result.get("total_score", 0) - grade = eval_result.get("grade", "N/A") - - with st.expander(f"**{vendor_name}** - Score: {total_score:.1f} ({grade})", expanded=i==0): - # Score summary - col1, col2, col3 = st.columns(3) - with col1: - st.metric("Total Score", f"{total_score:.1f}") - with col2: - st.metric("Grade", grade) - with col3: - st.metric("File", proposal_file[:25] + "..." if len(proposal_file) > 25 else proposal_file) - - # Criterion scores with justifications - st.markdown("#### Criterion Scores & Justifications") - criterion_scores = eval_result.get("criterion_scores", []) - for cs in criterion_scores: - score_pct = cs.get("raw_score", 0) - bar_color = "๐ŸŸข" if score_pct >= 80 else "๐ŸŸก" if score_pct >= 60 else "๐ŸŸ " if score_pct >= 40 else "๐Ÿ”ด" - criterion_name = cs.get('criterion_name', 'Unknown') - weighted_score = cs.get('weighted_score', 0) - justification = cs.get('justification', '') - - # Show criterion score with expandable justification - with st.container(): - st.markdown(f"{bar_color} **{criterion_name}**: {score_pct:.1f}/100 (weighted: {weighted_score:.2f})") - if justification: - with st.expander("๐Ÿ“ View Justification", expanded=False): - st.markdown(justification) - - # Show strengths and gaps for this criterion if available - strengths = cs.get('strengths', []) - gaps = cs.get('gaps', []) - if strengths or gaps: - col_s, col_g = st.columns(2) - with col_s: - if strengths: - st.markdown("**Strengths:**") - for s in strengths[:3]: - st.markdown(f" โœ… {s}") - with col_g: - if gaps: - st.markdown("**Gaps:**") - for g in gaps[:3]: - st.markdown(f" โš ๏ธ {g}") - st.markdown("---") - - # Overall Strengths and weaknesses - col1, col2 = st.columns(2) - with col1: - st.markdown("#### Overall Strengths") - for s in eval_result.get("overall_strengths", []): - st.markdown(f"โœ… {s}") - with col2: - st.markdown("#### Overall Weaknesses") - for w in eval_result.get("overall_weaknesses", []): - st.markdown(f"โš ๏ธ {w}") - - # Executive summary - st.markdown("#### Executive Summary") - st.markdown(eval_result.get("executive_summary", "No summary available.")) - - -def render_detailed_scores(evaluations: list): - """Render detailed score comparison table.""" - st.subheader("๐Ÿ“ˆ Detailed Score Comparison") - - if not evaluations: - st.warning("No evaluations available.") - return - - # Build comparison data - # Get all criteria from first evaluation - criteria = [] - if evaluations[0].get("criterion_scores"): - criteria = [cs.get("criterion_name", cs.get("criterion_id", f"C-{i}")) - for i, cs in enumerate(evaluations[0]["criterion_scores"])] - - # Create comparison table data - table_data = [] - for criterion_idx, criterion_name in enumerate(criteria): - row = {"Criterion": criterion_name} - for eval_result in evaluations: - vendor_name = eval_result.get("supplier_name", "Unknown")[:15] - scores = eval_result.get("criterion_scores", []) - if criterion_idx < len(scores): - row[vendor_name] = f"{scores[criterion_idx].get('raw_score', 0):.1f}" - else: - row[vendor_name] = "N/A" - table_data.append(row) - - # Add total row - total_row = {"Criterion": "**TOTAL SCORE**"} - for eval_result in evaluations: - vendor_name = eval_result.get("supplier_name", "Unknown")[:15] - total_row[vendor_name] = f"**{eval_result.get('total_score', 0):.1f}**" - table_data.append(total_row) - - st.table(table_data) - - -def render_export_options(comparison: dict, evaluations: list): - """Render export options for reports.""" - st.subheader("๐Ÿ“ฅ Export Reports") - - # Full Analysis Report (with comparison) - st.markdown("### ๐Ÿ“‘ Full Analysis Report") - st.markdown("Complete report including comparison, rankings, and all vendor details.") - - if comparison and evaluations: - full_report = generate_full_analysis_report(comparison, evaluations) - if full_report: - st.download_button( - label="๐Ÿ“‘ Download Full Analysis Report (Word)", - data=full_report, - file_name="rfp_full_analysis_report.docx", - mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document", - use_container_width=True, - key="full_analysis_report" - ) - else: - st.caption("Word export not available. Install python-docx with: pip install python-docx") - - st.markdown("---") - - col1, col2, col3 = st.columns(3) - - with col1: - st.markdown("### ๐Ÿ“Š CSV Comparison") - if comparison and evaluations: - comparison_agent = ComparisonAgent() - csv_content = comparison_agent.generate_csv_report(comparison, evaluations) - st.download_button( - label="๐Ÿ“Š Download CSV", - data=csv_content, - file_name="vendor_comparison.csv", - mime="text/csv", - use_container_width=True - ) - - with col2: - st.markdown("### ๐Ÿ“‹ JSON Data") - full_data = { - "comparison": comparison, - "evaluations": evaluations - } - st.download_button( - label="๐Ÿ“‹ Download JSON", - data=json.dumps(full_data, indent=2), - file_name="evaluation_data.json", - mime="application/json", - use_container_width=True - ) - - with col3: - st.markdown("### ๐Ÿ“„ Individual Reports") - st.caption("Detailed Word report for each vendor") - - # Individual vendor reports in a separate section - st.markdown("### ๐Ÿ“„ Individual Vendor Reports (Word)") - st.markdown("Detailed reports with criterion justifications for each vendor.") - - vendor_cols = st.columns(min(len(evaluations), 4)) - for i, eval_result in enumerate(evaluations): - col_idx = i % min(len(evaluations), 4) - with vendor_cols[col_idx]: - vendor_name = eval_result.get("supplier_name", f"Vendor_{i+1}").replace(" ", "_") - word_doc = generate_word_report(eval_result) - if word_doc: - st.download_button( - label=f"๐Ÿ“„ {vendor_name[:20]}", - data=word_doc, - file_name=f"report_{vendor_name}.docx", - mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document", - use_container_width=True, - key=f"word_{i}" - ) - else: - st.caption(f"Not available for {vendor_name[:15]}") - - -def render_results(results: dict): - """Render the evaluation results in a comprehensive markdown report format.""" - st.markdown("---") - - # Display timing information if available - if st.session_state.step_durations: - render_timing_summary(st.session_state.step_durations, results) - - # Quick score summary at the top - render_score_summary(results) - - # Generate and display the markdown report - report_md = generate_score_report(results) - - # Display report in tabs - tab1, tab2 = st.tabs(["๐Ÿ“Š Score Report", "๐Ÿ“‹ Raw Data"]) - - with tab1: - st.markdown(report_md) - - with tab2: - st.json(results) - - # Download buttons section - st.markdown("---") - st.subheader("๐Ÿ“ฅ Download Report") - - response_id = results.get('response_id', 'report') - supplier_name = results.get('supplier_name', 'vendor').replace(' ', '_') - - col1, col2, col3 = st.columns(3) - - with col1: - st.download_button( - label="๐Ÿ“„ Download Markdown", - data=report_md, - file_name=f"rfp_score_report_{response_id}.md", - mime="text/markdown", - use_container_width=True, - help="Download the report as a Markdown file" - ) - - with col2: - import json - st.download_button( - label="๐Ÿ“‹ Download JSON", - data=json.dumps(results, indent=2), - file_name=f"rfp_score_report_{response_id}.json", - mime="application/json", - use_container_width=True, - help="Download the raw data as JSON" - ) - - with col3: - if PDF_AVAILABLE and MARKDOWN_AVAILABLE: - # Generate PDF - pdf_data = generate_pdf_from_markdown( - report_md, - title=f"RFP Score Report - {supplier_name}" - ) - if pdf_data: - st.download_button( - label="๐Ÿ“‘ Download PDF", - data=pdf_data, - file_name=f"rfp_score_report_{response_id}.pdf", - mime="application/pdf", - use_container_width=True, - help="Download the report as a PDF file" - ) - else: - st.button( - "๐Ÿ“‘ PDF Generation Failed", - disabled=True, - use_container_width=True, - help="PDF generation encountered an error" - ) - else: - st.button( - "๐Ÿ“‘ PDF Not Available", - disabled=True, - use_container_width=True, - help="Install 'weasyprint' and 'markdown' packages to enable PDF export" - ) - - -def render_results_v2(results: dict): - """Render the V2 multi-agent evaluation results.""" - st.markdown("---") - - # Display timing information if available - if st.session_state.step_durations: - render_timing_summary_v2(st.session_state.step_durations, results) - - # Quick score summary - render_score_summary_v2(results) - - # Generate and display the markdown report - report_md = generate_score_report_v2(results) - - # Display report in tabs - tab1, tab2, tab3 = st.tabs(["๐Ÿ“Š Score Report", "๐Ÿ“‹ Extracted Criteria", "๐Ÿ“„ Raw Data"]) - - with tab1: - st.markdown(report_md) - - with tab2: - render_extracted_criteria(results) - - with tab3: - st.json(results) - - # Download buttons - st.markdown("---") - st.subheader("๐Ÿ“ฅ Download Report") - - response_id = results.get('response_id', 'report') - supplier_name = results.get('supplier_name', 'vendor').replace(' ', '_') - - col1, col2, col3 = st.columns(3) - - with col1: - st.download_button( - label="๐Ÿ“„ Download Markdown", - data=report_md, - file_name=f"rfp_score_report_v2_{response_id}.md", - mime="text/markdown", - use_container_width=True - ) - - with col2: - import json - st.download_button( - label="๐Ÿ“‹ Download JSON", - data=json.dumps(results, indent=2), - file_name=f"rfp_score_report_v2_{response_id}.json", - mime="application/json", - use_container_width=True - ) - - with col3: - if PDF_AVAILABLE and MARKDOWN_AVAILABLE: - pdf_data = generate_pdf_from_markdown(report_md, title=f"RFP Score Report V2 - {supplier_name}") - if pdf_data: - st.download_button( - label="๐Ÿ“‘ Download PDF", - data=pdf_data, - file_name=f"rfp_score_report_v2_{response_id}.pdf", - mime="application/pdf", - use_container_width=True - ) - else: - st.button("๐Ÿ“‘ PDF Generation Failed", disabled=True, use_container_width=True) - else: - st.button("๐Ÿ“‘ PDF Not Available", disabled=True, use_container_width=True) - - -def render_timing_summary_v2(durations: dict, results: dict): - """Render timing summary for V2 multi-agent evaluation.""" - st.subheader("โฑ๏ธ Multi Agents Evaluation Timing") - - metadata = results.get("_metadata", {}) - time_saved = durations.get("parallel_time_saved", 0) - docs_parallel_total = durations.get("docs_parallel_total", 0) - - # Row 1: Document processing - st.markdown("**Document Processing** (parallel)") - col1, col2, col3 = st.columns(3) - - with col1: - rfp_time = durations.get("rfp_processing", 0) - st.metric(label="๐Ÿ“„ RFP Processing", value=format_duration(rfp_time)) - - with col2: - proposal_time = durations.get("proposal_processing", 0) - st.metric(label="๐Ÿ“ Proposal Processing", value=format_duration(proposal_time)) - - with col3: - st.metric( - label="โšก Parallel Total", - value=format_duration(docs_parallel_total), - delta=f"-{format_duration(time_saved)} saved" if time_saved > 1 else None, - delta_color="inverse" - ) - - # Row 2: AI Scoring phases - st.markdown("**AI Evaluation** (multi agents)") - col1, col2, col3 = st.columns(3) - - with col1: - phase1_time = metadata.get("phase1_criteria_extraction_seconds", 0) - criteria_count = metadata.get("criteria_count", 0) - st.metric( - label="๐Ÿ” 3a. Criteria Extraction", - value=format_duration(phase1_time), - delta=f"{criteria_count} criteria" if criteria_count else None, - delta_color="off" - ) - - with col2: - phase2_time = metadata.get("phase2_proposal_scoring_seconds", 0) - st.metric(label="๐Ÿ“Š 3b. Proposal Scoring", value=format_duration(phase2_time)) - - with col3: - total_time = durations.get("total", 0) - st.metric(label="โฑ๏ธ Total Pipeline", value=format_duration(total_time)) - - if metadata: - with st.expander("๐Ÿ” Detailed Timing & Model Info", expanded=False): - col1, col2 = st.columns(2) - with col1: - st.markdown(f""" - **Evaluation Details:** - - Version: `{metadata.get('version', 'N/A')}` - - Type: `{metadata.get('evaluation_type', 'N/A')}` - - Timestamp: `{metadata.get('evaluation_timestamp', 'N/A')}` - - Model: `{metadata.get('model_deployment', 'N/A')}` - - Analysis Depth: `{metadata.get('reasoning_effort', 'N/A')}` - """) - with col2: - st.markdown(f""" - **Multi Agents Timing:** - - Criteria Extraction (Agent 1): `{format_duration(metadata.get('phase1_criteria_extraction_seconds', 0))}` - - Proposal Scoring (Agent 2): `{format_duration(metadata.get('phase2_proposal_scoring_seconds', 0))}` - - Criteria Found: `{metadata.get('criteria_count', 0)}` - - Time Saved (parallel docs): `{format_duration(time_saved)}` - """) - - st.markdown("---") - - -def render_score_summary_v2(results: dict): - """Render a visual score summary header for V2.""" - total_score = results.get("total_score", 0) - grade = results.get("grade", "N/A") - supplier_name = results.get("supplier_name", "Unknown Vendor") - recommendation = results.get("recommendation", "No recommendation") - criteria_count = results.get("_metadata", {}).get("criteria_count", 0) - - # Grade color mapping - grade_colors = {"A": "green", "B": "blue", "C": "orange", "D": "red", "F": "red"} - grade_color = grade_colors.get(grade, "gray") - - col1, col2, col3, col4 = st.columns(4) - - with col1: - st.metric(label="Supplier", value=supplier_name[:20] + "..." if len(supplier_name) > 20 else supplier_name) - - with col2: - st.metric(label="Total Score", value=f"{total_score:.1f}/100", help="Weighted sum of all criterion scores") - - with col3: - st.metric(label="Criteria Evaluated", value=str(criteria_count)) - - with col4: - st.markdown(f""" -
-

Grade

-

{grade}

-
- """, unsafe_allow_html=True) - - # Recommendation banner - if "recommend" in recommendation.lower() and "not" not in recommendation.lower(): - st.success(f"โœ… **Recommendation:** {recommendation}") - elif "not recommend" in recommendation.lower(): - st.error(f"โŒ **Recommendation:** {recommendation}") - else: - st.info(f"โ„น๏ธ **Recommendation:** {recommendation}") - - st.markdown("---") - - -def render_extracted_criteria(results: dict): - """Render the extracted criteria from the RFP.""" - extracted = results.get("extracted_criteria", {}) - criteria = extracted.get("criteria", []) - - st.subheader("๐Ÿ” Criteria Extracted from RFP") - - if extracted.get("rfp_summary"): - st.info(f"**RFP Summary:** {extracted.get('rfp_summary')}") - - if criteria: - st.markdown(f"**Total Criteria:** {len(criteria)} | **Total Weight:** {extracted.get('total_weight', 100)}%") - - # Display as a table - criteria_data = [] - for c in criteria: - criteria_data.append({ - "ID": c.get("criterion_id", ""), - "Name": c.get("name", ""), - "Category": c.get("category", ""), - "Weight": f"{c.get('weight', 0):.1f}%", - "Description": c.get("description", "")[:100] + "..." if len(c.get("description", "")) > 100 else c.get("description", "") - }) - - st.dataframe(criteria_data, use_container_width=True) - - # Detailed view in expander - with st.expander("๐Ÿ“‹ Detailed Criteria Descriptions", expanded=False): - for c in criteria: - st.markdown(f""" - ### {c.get('criterion_id', '')}. {c.get('name', '')} ({c.get('weight', 0):.1f}%) - - **Category:** {c.get('category', 'N/A')} - - **Description:** {c.get('description', 'N/A')} - - **Evaluation Guidance:** {c.get('evaluation_guidance', 'N/A')} - - --- - """) - else: - st.warning("No criteria extracted.") - - if extracted.get("extraction_notes"): - st.caption(f"**Notes:** {extracted.get('extraction_notes')}") - - -def generate_score_report_v2(results: dict) -> str: - """Generate a comprehensive markdown score report for V2.""" - - # Extract data - rfp_title = results.get("rfp_title", "RFP Evaluation") - supplier_name = results.get("supplier_name", "Unknown Vendor") - supplier_site = results.get("supplier_site", "N/A") - response_id = results.get("response_id", "N/A") - evaluation_date = results.get("evaluation_date", "N/A") - total_score = results.get("total_score", 0) - grade = results.get("grade", "N/A") - recommendation = results.get("recommendation", "N/A") - - extracted = results.get("extracted_criteria", {}) - criterion_scores = results.get("criterion_scores", []) - - executive_summary = results.get("executive_summary", "") - overall_strengths = results.get("overall_strengths", []) - overall_weaknesses = results.get("overall_weaknesses", []) - recommendations = results.get("recommendations", []) - risk_assessment = results.get("risk_assessment", "") - - # Grade badge - grade_badges = { - "A": "๐ŸŸข **EXCELLENT**", - "B": "๐Ÿ”ต **GOOD**", - "C": "๐ŸŸก **ACCEPTABLE**", - "D": "๐ŸŸ  **BELOW AVERAGE**", - "F": "๐Ÿ”ด **POOR**" - } - grade_badge = grade_badges.get(grade, "โšช **UNKNOWN**") - - # Build the report - report = f"""# ๐Ÿ“Š RFP Evaluation Report (V2 - Multi Agents) - ---- - -## ๐Ÿ“‹ Evaluation Summary - -| Field | Value | -|-------|-------| -| **RFP Title** | {rfp_title} | -| **Response ID** | {response_id} | -| **Supplier** | {supplier_name} | -| **Supplier Site** | {supplier_site} | -| **Evaluation Date** | {evaluation_date} | -| **Scoring Version** | V2 (Multi-Agent) | - ---- - -## ๐ŸŽฏ Score Overview - -| Metric | Value | -|--------|-------| -| **Total Score** | **{total_score:.2f}** / 100 | -| **Grade** | {grade_badge} | -| **Criteria Evaluated** | {len(criterion_scores)} | - -### Recommendation - -{recommendation} - ---- - -## ๐Ÿ” Extracted Criteria Summary - -**RFP Summary:** {extracted.get('rfp_summary', 'N/A')} - -**Criteria Count:** {extracted.get('criteria_count', len(extracted.get('criteria', [])))} - -| ID | Criterion | Category | Weight | -|----|-----------|----------|--------| -""" - - # Add criteria summary - for c in extracted.get("criteria", []): - report += f"| {c.get('criterion_id', '')} | {c.get('name', '')} | {c.get('category', '')} | {c.get('weight', 0):.1f}% |\n" - - report += """ ---- - -## ๐Ÿ“ˆ Detailed Scoring Results - -| Criterion | Weight | Raw Score | Weighted Score | -|-----------|--------|-----------|----------------| -""" - - # Add score rows - for cs in criterion_scores: - raw = cs.get("raw_score", 0) - weighted = cs.get("weighted_score", 0) - name = cs.get("criterion_name", cs.get("criterion_id", "")) - weight = cs.get("weight", 0) - - # Score indicator - if raw >= 80: - indicator = "๐ŸŸข" - elif raw >= 60: - indicator = "๐ŸŸก" - elif raw >= 40: - indicator = "๐ŸŸ " - else: - indicator = "๐Ÿ”ด" - - report += f"| {indicator} {name} | {weight:.1f}% | {raw:.1f} | **{weighted:.2f}** |\n" - - # Add total row - total_weighted = sum(cs.get("weighted_score", 0) for cs in criterion_scores) - report += f"| **TOTAL** | **100%** | - | **{total_weighted:.2f}** |\n" - - report += """ ---- - -## ๐Ÿ“ Criterion-by-Criterion Analysis - -""" - - # Detailed analysis for each criterion - for cs in criterion_scores: - criterion_id = cs.get("criterion_id", "") - criterion_name = cs.get("criterion_name", "") - raw_score = cs.get("raw_score", 0) - weighted_score = cs.get("weighted_score", 0) - weight = cs.get("weight", 0) - evidence = cs.get("evidence", "No evidence provided") - justification = cs.get("justification", "No justification provided") - strengths = cs.get("strengths", []) - gaps = cs.get("gaps", []) - - # Score indicator - if raw_score >= 80: - indicator = "๐ŸŸข" - elif raw_score >= 60: - indicator = "๐ŸŸก" - elif raw_score >= 40: - indicator = "๐ŸŸ " - else: - indicator = "๐Ÿ”ด" - - report += f"""### {indicator} {criterion_id}. {criterion_name} - -| Metric | Value | -|--------|-------| -| Raw Score | **{raw_score:.1f}** / 100 | -| Weight | {weight:.1f}% | -| Weighted Score | **{weighted_score:.2f}** | - -**Evidence from Proposal:** -> {evidence} - -**Justification:** -{justification} - -""" - - if strengths: - report += "**Strengths:**\n" - for s in strengths: - report += f"- โœ… {s}\n" - report += "\n" - - if gaps: - report += "**Gaps/Weaknesses:**\n" - for g in gaps: - report += f"- โš ๏ธ {g}\n" - report += "\n" - - report += "---\n\n" - - # Overall Analysis - report += """## ๐Ÿ’ก Overall Analysis - -### Key Strengths -""" - if overall_strengths: - for s in overall_strengths: - report += f"- โœ… {s}\n" - else: - report += "- No specific strengths identified\n" - - report += """ -### Key Weaknesses -""" - if overall_weaknesses: - for w in overall_weaknesses: - report += f"- โš ๏ธ {w}\n" - else: - report += "- No significant weaknesses identified\n" - - report += """ -### Recommendations -""" - if recommendations: - for i, r in enumerate(recommendations, 1): - report += f"{i}. {r}\n" - else: - report += "1. No specific recommendations at this time\n" - - # Risk Assessment - report += f""" ---- - -## โš ๏ธ Risk Assessment - -{risk_assessment if risk_assessment else "No risk assessment provided."} - ---- - -## ๐Ÿ“‹ Executive Summary - -{executive_summary if executive_summary else "No executive summary provided."} - ---- - -## ๐Ÿ“Š Grade Interpretation Guide - -| Grade | Score Range | Interpretation | -|-------|-------------|----------------| -| A | 90-100 | โœ… Excellent - Strongly recommended | -| B | 80-89 | โœ… Good - Recommended | -| C | 70-79 | โš ๏ธ Acceptable - Consider with improvements | -| D | 60-69 | ๐ŸŸ  Below Average - Significant concerns | -| F | Below 60 | โŒ Poor - Not recommended | - ---- - -*Report generated by RFP Analyzer V2 (Multi-Agent) - Powered by Azure Content Understanding & Microsoft Agent Framework* -""" - - # Add timing metadata if available - metadata = results.get("_metadata", {}) - if metadata: - phase1 = format_duration(metadata.get('phase1_criteria_extraction_seconds', 0)) - phase2 = format_duration(metadata.get('phase2_proposal_scoring_seconds', 0)) - total_eval = format_duration(metadata.get('total_duration_seconds', 0)) - - report += f""" ---- - -## โฑ๏ธ Evaluation Timing - -| Phase | Duration | -|-------|----------| -| **Criteria Extraction (Agent 1)** | {phase1} | -| **Proposal Scoring (Agent 2)** | {phase2} | -| **Total Evaluation Time** | {total_eval} | -| **Model Deployment** | {metadata.get('model_deployment', 'N/A')} | -| **Analysis Depth** | {metadata.get('reasoning_effort', 'N/A')} | -""" - - return report - - -def render_timing_summary(durations: dict, results: dict): - """Render a timing summary for all evaluation steps.""" - st.subheader("โฑ๏ธ Evaluation Timing Summary") - - # Get metadata from results if available - metadata = results.get("_metadata", {}) - - # Check if parallel processing was used - time_saved = durations.get("parallel_time_saved", 0) - docs_parallel_total = durations.get("docs_parallel_total", 0) - - col1, col2, col3, col4 = st.columns(4) - - with col1: - rfp_time = durations.get("rfp_processing", 0) - st.metric( - label="๐Ÿ“„ RFP Processing", - value=format_duration(rfp_time), - help="Time to extract content from RFP document" - ) - - with col2: - proposal_time = durations.get("proposal_processing", 0) - st.metric( - label="๐Ÿ“ Proposal Processing", - value=format_duration(proposal_time), - help="Time to extract content from Proposal document" - ) - - with col3: - scoring_time = durations.get("scoring", 0) - api_time = metadata.get("api_call_duration_seconds", scoring_time) - st.metric( - label="๐Ÿง  AI Scoring", - value=format_duration(scoring_time), - delta=f"API: {format_duration(api_time)}" if api_time != scoring_time else None, - help="Time for AI reasoning and scoring" - ) - - with col4: - total_time = durations.get("total", 0) - st.metric( - label="โฑ๏ธ Total Time", - value=format_duration(total_time), - delta=f"-{format_duration(time_saved)} saved" if time_saved > 1 else None, - delta_color="inverse", - help="Total evaluation pipeline duration" - ) - - # Show parallel processing info - if time_saved > 1: - st.success(f"โšก **Parallel Processing:** Documents processed simultaneously in {format_duration(docs_parallel_total)} (saved {format_duration(time_saved)})") - - # Additional metadata details - if metadata: - with st.expander("๐Ÿ” Detailed Timing & Model Info", expanded=False): - detail_col1, detail_col2 = st.columns(2) - with detail_col1: - st.markdown(f""" - **Evaluation Details:** - - Timestamp: `{metadata.get('evaluation_timestamp', 'N/A')}` - - Model: `{metadata.get('model_deployment', 'N/A')}` - - Analysis Depth: `{metadata.get('reasoning_effort', 'N/A')}` - """) - with detail_col2: - api_duration = metadata.get('api_call_duration_seconds', 0) - parse_duration = metadata.get('parse_duration_seconds', 0) - total_eval_duration = metadata.get('total_duration_seconds', 0) - st.markdown(f""" - **Duration Breakdown:** - - API Call: `{format_duration(api_duration)}` - - Response Parsing: `{format_duration(parse_duration)}` - - Total Evaluation: `{format_duration(total_eval_duration)}` - """) - - st.markdown("---") - - -def render_score_summary(results: dict): - """Render a visual score summary header.""" - requirement_score = results.get("requirement_score", 0) - composite_score = results.get("composite_score", 0) - supplier_name = results.get("supplier_name", "Unknown Vendor") - - # Determine recommendation based on composite score - if composite_score >= 60: - recommendation = "โœ… STRONGLY RECOMMENDED" - color = "green" - elif composite_score >= 50: - recommendation = "โœ… RECOMMENDED" - color = "green" - elif composite_score >= 40: - recommendation = "โš ๏ธ REVIEW REQUIRED" - color = "orange" - else: - recommendation = "โŒ NOT RECOMMENDED" - color = "red" - - # Display score cards - col1, col2, col3, col4 = st.columns(4) - - with col1: - st.metric( - label="Supplier", - value=supplier_name[:20] + "..." if len(supplier_name) > 20 else supplier_name - ) - - with col2: - st.metric( - label="Requirement Score", - value=f"{requirement_score:.2f}", - help="Total score out of 100" - ) - - with col3: - st.metric( - label="Composite Score", - value=f"{composite_score:.2f}", - help="Weighted score out of 70" - ) - - with col4: - st.markdown( - f""" -
-

Recommendation

-

{recommendation}

-
- """, - unsafe_allow_html=True - ) - - st.markdown("---") - - -def generate_score_report(results: dict) -> str: - """Generate a comprehensive markdown score report matching the RFP scoring format.""" - - # Extract data - rfp_title = results.get("rfp_title", "RFP Evaluation") - supplier_name = results.get("supplier_name", "Unknown Vendor") - supplier_site = results.get("supplier_site", "N/A") - response_id = results.get("response_id", "N/A") - scoring_status = results.get("scoring_status", "Completed") - requirement_score = results.get("requirement_score", 0) - composite_score = results.get("composite_score", 0) - overall_rank = results.get("overall_rank", 1) - requirements = results.get("requirements", []) - strengths = results.get("strengths", []) - weaknesses = results.get("weaknesses", []) - recommendations = results.get("recommendations", []) - summary = results.get("summary", "") - - # Determine recommendation based on score - if composite_score >= 60: - recommendation_badge = "โœ… **RECOMMENDED**" - recommendation_color = "green" - elif composite_score >= 50: - recommendation_badge = "โš ๏ธ **CONDITIONALLY RECOMMENDED**" - recommendation_color = "orange" - elif composite_score >= 40: - recommendation_badge = "๐Ÿ”ถ **REVIEW REQUIRED**" - recommendation_color = "yellow" - else: - recommendation_badge = "โŒ **NOT RECOMMENDED**" - recommendation_color = "red" - - # Build the report - report = f"""# ๐Ÿ“Š Requirement Scores Report - ---- - -## ๐Ÿ“‹ Evaluation Summary - -| Field | Value | -|-------|-------| -| **Title** | {rfp_title} | -| **Response** | {response_id} | -| **Supplier** | {supplier_name} | -| **Supplier Site** | {supplier_site} | -| **Scoring Status** | {scoring_status} | - ---- - -## ๐ŸŽฏ Score Overview - -| Metric | Score | -|--------|-------| -| **Requirement Score** | **{requirement_score:.2f}** / 100 | -| **Composite Score** | **{composite_score:.2f}** / 70 | -| **Overall Rank (Composite)** | {overall_rank} | -| **Recommendation** | {recommendation_badge} | - ---- - -## ๐Ÿ“ˆ Technical Evaluation Criteria - -| Requirement | Requirement Text | Evaluation Stage | Target Value | Response Value | Maximum Score | Score | Weight | Weighted Score | -|-------------|------------------|------------------|--------------|----------------|---------------|-------|--------|----------------| -""" - - # Add requirement rows - for req in requirements: - req_id = req.get("requirement_id", "") - req_name = req.get("requirement_name", "") - req_text = req.get("requirement_text", "") - eval_stage = req.get("evaluation_stage", "Technical") - target_val = req.get("target_value", "") - response_val = req.get("response_value", "")[:50] + "..." if len(req.get("response_value", "")) > 50 else req.get("response_value", "") - max_score = req.get("maximum_score", 20) - score = req.get("score", 0) - weight = req.get("weight", 14.0) - weighted_score = req.get("weighted_score", 0) - - report += f"| **{req_id}. {req_name}** | {req_text} | {eval_stage} | {target_val} | {response_val} | {max_score} | **{score:.2f}** | {weight:.0f}% | **{weighted_score:.2f}** |\n" - - # Add totals row - total_max = sum(req.get("maximum_score", 20) for req in requirements) if requirements else 100 - total_score = sum(req.get("score", 0) for req in requirements) if requirements else requirement_score - total_weight = sum(req.get("weight", 14.0) for req in requirements) if requirements else 70 - total_weighted = sum(req.get("weighted_score", 0) for req in requirements) if requirements else composite_score - - report += f"| **TOTAL** | | | | | **{total_max}** | **{total_score:.2f}** | **{total_weight:.0f}%** | **{total_weighted:.2f}** |\n" - - report += """ ---- - -## ๐Ÿ“ Detailed Requirement Analysis - -""" - - # Detailed analysis for each requirement - for req in requirements: - req_id = req.get("requirement_id", "") - req_name = req.get("requirement_name", "") - score = req.get("score", 0) - max_score = req.get("maximum_score", 20) - weighted_score = req.get("weighted_score", 0) - comments = req.get("comments", "No comments provided") - response_value = req.get("response_value", "") - - # Score indicator - pct = (score / max_score * 100) if max_score > 0 else 0 - if pct >= 85: - indicator = "๐ŸŸข" - elif pct >= 65: - indicator = "๐ŸŸก" - elif pct >= 45: - indicator = "๐ŸŸ " - else: - indicator = "๐Ÿ”ด" - - report += f"""### {indicator} {req_id}. {req_name} - -| Metric | Value | -|--------|-------| -| Score | **{score:.2f}** / {max_score} | -| Weighted Score | **{weighted_score:.2f}** | -| Performance | {pct:.0f}% | - -**Response Summary:** {response_value} - -**Evaluation Comments:** {comments} - ---- - -""" - - # Strengths section - report += """## โœ… Key Strengths - -""" - if strengths: - for strength in strengths: - report += f"- {strength}\n" - else: - report += "- No specific strengths identified\n" - - # Weaknesses section - report += """ ---- - -## โš ๏ธ Areas for Improvement - -""" - if weaknesses: - for weakness in weaknesses: - report += f"- {weakness}\n" - else: - report += "- No significant weaknesses identified\n" - - # Recommendations section - report += """ ---- - -## ๐Ÿ’ก Recommendations - -""" - if recommendations: - for i, rec in enumerate(recommendations, 1): - report += f"{i}. {rec}\n" - else: - report += "1. No specific recommendations at this time\n" - - # Executive Summary - report += f""" ---- - -## ๐Ÿ“‹ Executive Summary - -{summary if summary else "No executive summary provided."} - ---- - -## ๐Ÿ“Š Score Interpretation Guide - -| Weighted Score Range | Rating | Recommendation | -|---------------------|--------|----------------| -| 60-70 | Excellent | โœ… Strongly recommended for selection | -| 50-59 | Very Good | โœ… Recommended with minor clarifications | -| 40-49 | Good | โš ๏ธ Consider with some negotiation | -| 30-39 | Acceptable | ๐Ÿ”ถ Review concerns before proceeding | -| Below 30 | Poor | โŒ Not recommended | - ---- - -*Report generated by RFP Analyzer - Powered by Azure Content Understanding & Microsoft Agent Framework* -""" - - # Add timing metadata if available - metadata = results.get("_metadata", {}) - if metadata: - api_duration = format_duration(metadata.get('api_call_duration_seconds', 0)) - total_eval_duration = format_duration(metadata.get('total_duration_seconds', 0)) - report += f""" ---- - -## โฑ๏ธ Evaluation Timing - -| Metric | Value | -|--------|-------| -| **Evaluation Timestamp** | {metadata.get('evaluation_timestamp', 'N/A')} | -| **Model Deployment** | {metadata.get('model_deployment', 'N/A')} | -| **Analysis Depth** | {metadata.get('reasoning_effort', 'N/A')} | -| **API Call Duration** | {api_duration} | -| **Total Evaluation Time** | {total_eval_duration} | -""" - - return report - - -def main(): - """Main application entry point.""" - render_sidebar() - - # Render current step - if st.session_state.step == 0: - render_landing_page() - elif st.session_state.step == 1: - render_step1() - elif st.session_state.step == 2: - render_step2() - elif st.session_state.step == 3: - render_step3() - - -if __name__ == "__main__": - main() diff --git a/app/pyproject.toml b/app/pyproject.toml deleted file mode 100644 index df5a8a3..0000000 --- a/app/pyproject.toml +++ /dev/null @@ -1,35 +0,0 @@ -[project] -name = "rfp-analyzer" -version = "0.1.0" -description = "RFP Analyzer using Azure Content Understanding and Microsoft Agent Framework" -readme = "README.md" -requires-python = ">=3.13" -dependencies = [ - "streamlit>=1.52.2", - "agent-framework", - "azure-identity>=1.19.0", - "azure-ai-documentintelligence>=1.0.2", - "python-dotenv>=1.2.1", - "pydantic>=2.12.5", - "openai>=2.15.0", - "requests>=2.32.5", - "azure-monitor-opentelemetry>=1.8.3", - "opentelemetry-sdk>=1.39.0", - "azure-core-tracing-opentelemetry>=1.0.0b12", - "python-docx>=1.1.0", - "plotly>=6.5.2", -] - -[project.optional-dependencies] -pdf = [ - "weasyprint>=62.0", - "markdown>=3.7", -] - -[tool.uv] -prerelease = "allow" - -[dependency-groups] -dev = [ - "watchdog>=6.0.0", -] diff --git a/app/requirements.txt b/app/requirements.txt deleted file mode 100644 index 60e08d9..0000000 --- a/app/requirements.txt +++ /dev/null @@ -1,651 +0,0 @@ -# This file was autogenerated by uv via the following command: -# uv export --no-hashes -a2a-sdk==0.3.22 - # via agent-framework-a2a -ag-ui-protocol==0.1.10 - # via agent-framework-ag-ui -agent-framework==1.0.0b260107 - # via rfp-analyzer -agent-framework-a2a==1.0.0b260107 - # via agent-framework-core -agent-framework-ag-ui==1.0.0b260107 - # via agent-framework-core -agent-framework-anthropic==1.0.0b260107 - # via agent-framework-core -agent-framework-azure-ai==1.0.0b260107 - # via agent-framework-core -agent-framework-azure-ai-search==1.0.0b260107 - # via agent-framework-core -agent-framework-azurefunctions==1.0.0b260107 - # via agent-framework-core -agent-framework-chatkit==1.0.0b260107 - # via agent-framework-core -agent-framework-copilotstudio==1.0.0b260107 - # via agent-framework-core -agent-framework-core==1.0.0b260107 - # via - # agent-framework - # agent-framework-a2a - # agent-framework-ag-ui - # agent-framework-anthropic - # agent-framework-azure-ai - # agent-framework-azure-ai-search - # agent-framework-azurefunctions - # agent-framework-chatkit - # agent-framework-copilotstudio - # agent-framework-declarative - # agent-framework-devui - # agent-framework-lab - # agent-framework-mem0 - # agent-framework-ollama - # agent-framework-purview - # agent-framework-redis -agent-framework-declarative==1.0.0b260107 - # via agent-framework-core -agent-framework-devui==1.0.0b260107 - # via agent-framework-core -agent-framework-lab==1.0.0b251024 - # via agent-framework-core -agent-framework-mem0==1.0.0b260107 - # via agent-framework-core -agent-framework-ollama==1.0.0b260107 - # via agent-framework-core -agent-framework-purview==1.0.0b260107 - # via agent-framework-core -agent-framework-redis==1.0.0b260107 - # via agent-framework-core -aiohappyeyeballs==2.6.1 - # via aiohttp -aiohttp==3.13.3 - # via - # agent-framework-azure-ai - # azure-functions-durable -aiosignal==1.4.0 - # via aiohttp -altair==6.1.0.dev20260112 - # via streamlit -annotated-doc==0.0.4 - # via fastapi -annotated-types==0.7.0 - # via pydantic -anthropic==0.75.0 - # via agent-framework-anthropic -anyio==4.12.1 - # via - # anthropic - # httpx - # mcp - # openai - # sse-starlette - # starlette - # watchfiles -asgiref==3.11.0 - # via opentelemetry-instrumentation-asgi -attrs==25.4.0 - # via - # aiohttp - # jsonschema - # referencing -azure-ai-agents==1.2.0b5 - # via agent-framework-azure-ai -azure-ai-documentintelligence==1.0.2 - # via rfp-analyzer -azure-ai-projects==2.0.0b3 - # via agent-framework-azure-ai -azure-common==1.1.28 - # via azure-search-documents -azure-core==1.37.0 - # via - # agent-framework-purview - # azure-ai-agents - # azure-ai-documentintelligence - # azure-ai-projects - # azure-core-tracing-opentelemetry - # azure-identity - # azure-monitor-opentelemetry - # azure-monitor-opentelemetry-exporter - # azure-search-documents - # azure-storage-blob - # microsoft-agents-hosting-core - # msrest -azure-core-tracing-opentelemetry==1.0.0b12 - # via - # azure-monitor-opentelemetry - # rfp-analyzer -azure-functions==1.25.0b3.dev1 - # via - # agent-framework-azurefunctions - # azure-functions-durable -azure-functions-durable==1.4.0 - # via agent-framework-azurefunctions -azure-identity==1.26.0b1 - # via - # agent-framework-core - # azure-ai-projects - # azure-monitor-opentelemetry-exporter - # rfp-analyzer -azure-monitor-opentelemetry==1.8.3 - # via rfp-analyzer -azure-monitor-opentelemetry-exporter==1.0.0b46 - # via azure-monitor-opentelemetry -azure-search-documents==11.7.0b2 - # via agent-framework-azure-ai-search -azure-storage-blob==12.28.0 - # via azure-ai-projects -backoff==2.2.1 - # via posthog -blinker==1.9.0 - # via streamlit -cachetools==6.2.4 - # via streamlit -certifi==2026.1.4 - # via - # httpcore - # httpx - # msrest - # requests -cffi==2.0.0 ; python_full_version < '3.14' or platform_python_implementation != 'PyPy' - # via - # clr-loader - # cryptography - # powerfx -charset-normalizer==3.4.4 - # via requests -click==8.3.1 - # via - # streamlit - # uvicorn -clr-loader==0.2.10 ; python_full_version < '3.14' - # via pythonnet -colorama==0.4.6 - # via - # click - # griffe - # tqdm - # uvicorn -cryptography==46.0.3 - # via - # azure-identity - # azure-storage-blob - # msal - # pyjwt -distro==1.9.0 - # via - # anthropic - # openai - # posthog -docstring-parser==0.17.0 - # via anthropic -fastapi==0.128.0 - # via - # agent-framework-ag-ui - # agent-framework-devui -frozenlist==1.8.0 - # via - # aiohttp - # aiosignal -furl==2.1.4 - # via azure-functions-durable -gitdb==4.0.12 - # via gitpython -gitpython==3.1.46 - # via streamlit -google-api-core==2.29.0 - # via a2a-sdk -google-auth==2.47.0 - # via google-api-core -googleapis-common-protos==1.72.0 - # via google-api-core -greenlet==3.3.0 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' - # via sqlalchemy -griffe==1.15.0 - # via openai-agents -grpcio==1.76.0 - # via qdrant-client -h11==0.16.0 - # via - # httpcore - # uvicorn -h2==4.3.0 - # via httpx -hpack==4.1.0 - # via h2 -httpcore==1.0.9 - # via httpx -httptools==0.7.1 - # via uvicorn -httpx==0.28.1 - # via - # a2a-sdk - # agent-framework-purview - # anthropic - # mcp - # ollama - # openai - # qdrant-client -httpx-sse==0.4.3 - # via - # a2a-sdk - # mcp -hyperframe==6.1.0 - # via h2 -idna==3.11 - # via - # anyio - # httpx - # requests - # yarl -importlib-metadata==8.7.1 - # via opentelemetry-api -isodate==0.7.2 - # via - # azure-ai-agents - # azure-ai-documentintelligence - # azure-ai-projects - # azure-search-documents - # azure-storage-blob - # microsoft-agents-hosting-core - # msrest -jinja2==3.1.6 - # via - # altair - # openai-chatkit - # pydeck -jiter==0.12.0 - # via - # anthropic - # openai -jsonpath-ng==1.7.0 - # via redisvl -jsonschema==4.26.0 - # via - # altair - # mcp -jsonschema-specifications==2025.9.1 - # via jsonschema -markupsafe==3.0.3 - # via - # jinja2 - # werkzeug -mcp==1.25.0 - # via - # agent-framework-core - # openai-agents -mem0ai==1.0.1 - # via agent-framework-mem0 -microsoft-agents-activity==0.7.0.dev12 - # via microsoft-agents-hosting-core -microsoft-agents-copilotstudio-client==0.7.0.dev12 - # via agent-framework-copilotstudio -microsoft-agents-hosting-core==0.7.0.dev12 - # via microsoft-agents-copilotstudio-client -ml-dtypes==0.5.4 - # via redisvl -msal==1.35.0b1 - # via - # azure-identity - # msal-extensions -msal-extensions==1.3.1 - # via azure-identity -msrest==0.7.1 - # via azure-monitor-opentelemetry-exporter -multidict==6.7.0 - # via - # aiohttp - # yarl -narwhals==2.15.0 - # via altair -numpy==2.4.1 - # via - # agent-framework-redis - # ml-dtypes - # pandas - # pydeck - # qdrant-client - # redisvl - # streamlit -oauthlib==3.3.1 - # via requests-oauthlib -ollama==0.6.1 - # via agent-framework-ollama -openai==2.15.0 - # via - # agent-framework-core - # azure-ai-projects - # mem0ai - # openai-agents - # openai-chatkit - # rfp-analyzer -openai-agents==0.6.5 - # via openai-chatkit -openai-chatkit==1.5.2 - # via agent-framework-chatkit -opentelemetry-api==1.39.0 - # via - # agent-framework-core - # azure-core-tracing-opentelemetry - # azure-functions-durable - # azure-monitor-opentelemetry-exporter - # opentelemetry-instrumentation - # opentelemetry-instrumentation-asgi - # opentelemetry-instrumentation-dbapi - # opentelemetry-instrumentation-django - # opentelemetry-instrumentation-fastapi - # opentelemetry-instrumentation-flask - # opentelemetry-instrumentation-psycopg2 - # opentelemetry-instrumentation-requests - # opentelemetry-instrumentation-urllib - # opentelemetry-instrumentation-urllib3 - # opentelemetry-instrumentation-wsgi - # opentelemetry-sdk - # opentelemetry-semantic-conventions -opentelemetry-instrumentation==0.60b0 - # via - # opentelemetry-instrumentation-asgi - # opentelemetry-instrumentation-dbapi - # opentelemetry-instrumentation-django - # opentelemetry-instrumentation-fastapi - # opentelemetry-instrumentation-flask - # opentelemetry-instrumentation-psycopg2 - # opentelemetry-instrumentation-requests - # opentelemetry-instrumentation-urllib - # opentelemetry-instrumentation-urllib3 - # opentelemetry-instrumentation-wsgi -opentelemetry-instrumentation-asgi==0.60b0 - # via opentelemetry-instrumentation-fastapi -opentelemetry-instrumentation-dbapi==0.60b0 - # via opentelemetry-instrumentation-psycopg2 -opentelemetry-instrumentation-django==0.60b0 - # via azure-monitor-opentelemetry -opentelemetry-instrumentation-fastapi==0.60b0 - # via azure-monitor-opentelemetry -opentelemetry-instrumentation-flask==0.60b0 - # via azure-monitor-opentelemetry -opentelemetry-instrumentation-psycopg2==0.60b0 - # via azure-monitor-opentelemetry -opentelemetry-instrumentation-requests==0.60b0 - # via azure-monitor-opentelemetry -opentelemetry-instrumentation-urllib==0.60b0 - # via azure-monitor-opentelemetry -opentelemetry-instrumentation-urllib3==0.60b0 - # via azure-monitor-opentelemetry -opentelemetry-instrumentation-wsgi==0.60b0 - # via - # opentelemetry-instrumentation-django - # opentelemetry-instrumentation-flask -opentelemetry-resource-detector-azure==0.1.5 - # via azure-monitor-opentelemetry -opentelemetry-sdk==1.39.0 - # via - # agent-framework-core - # azure-functions-durable - # azure-monitor-opentelemetry - # azure-monitor-opentelemetry-exporter - # opentelemetry-resource-detector-azure - # rfp-analyzer -opentelemetry-semantic-conventions==0.60b0 - # via - # opentelemetry-instrumentation - # opentelemetry-instrumentation-asgi - # opentelemetry-instrumentation-dbapi - # opentelemetry-instrumentation-django - # opentelemetry-instrumentation-fastapi - # opentelemetry-instrumentation-flask - # opentelemetry-instrumentation-requests - # opentelemetry-instrumentation-urllib - # opentelemetry-instrumentation-urllib3 - # opentelemetry-instrumentation-wsgi - # opentelemetry-sdk -opentelemetry-semantic-conventions-ai==0.4.13 - # via agent-framework-core -opentelemetry-util-http==0.60b0 - # via - # opentelemetry-instrumentation-asgi - # opentelemetry-instrumentation-django - # opentelemetry-instrumentation-fastapi - # opentelemetry-instrumentation-flask - # opentelemetry-instrumentation-requests - # opentelemetry-instrumentation-urllib - # opentelemetry-instrumentation-urllib3 - # opentelemetry-instrumentation-wsgi -orderedmultidict==1.0.2 - # via furl -packaging==26.0rc1 - # via - # agent-framework-core - # altair - # opentelemetry-instrumentation - # opentelemetry-instrumentation-flask - # streamlit -pandas==2.3.3 - # via streamlit -pillow==12.1.0 - # via streamlit -ply==3.11 - # via jsonpath-ng -portalocker==3.2.0 - # via qdrant-client -posthog==7.5.1 - # via mem0ai -powerfx==0.0.34 ; python_full_version < '3.14' - # via agent-framework-declarative -propcache==0.4.1 - # via - # aiohttp - # yarl -proto-plus==1.27.0 - # via google-api-core -protobuf==5.29.5 - # via - # a2a-sdk - # google-api-core - # googleapis-common-protos - # mem0ai - # proto-plus - # qdrant-client - # streamlit -psutil==7.2.1 - # via azure-monitor-opentelemetry-exporter -pyarrow==22.0.0 - # via streamlit -pyasn1==0.6.1 - # via - # pyasn1-modules - # rsa -pyasn1-modules==0.4.2 - # via google-auth -pycparser==2.23 ; (python_full_version < '3.14' and implementation_name != 'PyPy') or (implementation_name != 'PyPy' and platform_python_implementation != 'PyPy') - # via cffi -pydantic==2.12.5 - # via - # a2a-sdk - # ag-ui-protocol - # agent-framework-core - # anthropic - # fastapi - # mcp - # mem0ai - # microsoft-agents-activity - # ollama - # openai - # openai-agents - # openai-chatkit - # pydantic-settings - # qdrant-client - # redisvl - # rfp-analyzer -pydantic-core==2.41.5 - # via pydantic -pydantic-settings==2.12.0 - # via - # agent-framework-core - # mcp -pydeck==0.9.1 - # via streamlit -pyjwt==2.10.1 - # via - # mcp - # microsoft-agents-hosting-core - # msal -python-dateutil==2.9.0.post0 - # via - # azure-functions-durable - # pandas - # posthog -python-dotenv==1.2.1 - # via - # agent-framework-devui - # microsoft-agents-hosting-core - # pydantic-settings - # rfp-analyzer - # uvicorn -python-multipart==0.0.21 - # via mcp -python-ulid==3.1.0 - # via redisvl -pythonnet==3.0.5 ; python_full_version < '3.14' - # via powerfx -pytz==2025.2 - # via - # mem0ai - # pandas -pywin32==311 ; sys_platform == 'win32' - # via - # mcp - # portalocker -pyyaml==6.0.3 - # via - # agent-framework-declarative - # redisvl - # uvicorn -qdrant-client==1.16.2 - # via mem0ai -redis==7.1.0 - # via - # agent-framework-redis - # redisvl -redisvl==0.13.2 - # via agent-framework-redis -referencing==0.37.0 - # via - # jsonschema - # jsonschema-specifications -requests==2.32.5 - # via - # azure-core - # azure-functions-durable - # google-api-core - # msal - # msrest - # openai-agents - # posthog - # requests-oauthlib - # rfp-analyzer - # streamlit -requests-oauthlib==2.0.0 - # via msrest -rpds-py==0.30.0 - # via - # jsonschema - # referencing -rsa==4.9.1 - # via google-auth -six==1.17.0 - # via - # furl - # orderedmultidict - # posthog - # python-dateutil -smmap==5.0.2 - # via gitdb -sniffio==1.3.1 - # via - # anthropic - # openai -sqlalchemy==2.0.45 - # via mem0ai -sse-starlette==3.1.2 - # via mcp -starlette==0.50.0 - # via - # fastapi - # mcp - # sse-starlette -streamlit==1.52.2 - # via rfp-analyzer -tenacity==9.1.2 - # via - # redisvl - # streamlit -toml==0.10.2 - # via streamlit -tornado==6.5.4 - # via streamlit -tqdm==4.67.1 - # via openai -types-requests==2.32.4.20260107 - # via openai-agents -typing-extensions==4.15.0 - # via - # agent-framework-core - # altair - # anthropic - # azure-ai-agents - # azure-ai-documentintelligence - # azure-core - # azure-identity - # azure-search-documents - # azure-storage-blob - # fastapi - # grpcio - # mcp - # openai - # openai-agents - # opentelemetry-api - # opentelemetry-sdk - # opentelemetry-semantic-conventions - # posthog - # pydantic - # pydantic-core - # sqlalchemy - # streamlit - # typing-inspection -typing-inspection==0.4.2 - # via - # mcp - # pydantic - # pydantic-settings -tzdata==2025.3 - # via pandas -urllib3==2.6.3 - # via - # qdrant-client - # requests - # types-requests -uvicorn==0.40.0 - # via - # agent-framework-ag-ui - # agent-framework-devui - # mcp - # openai-chatkit -uvloop==0.22.1 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32' - # via uvicorn -watchdog==6.0.0 - # via streamlit -watchfiles==1.1.1 - # via uvicorn -websockets==16.0 - # via - # mcp - # uvicorn -werkzeug==3.1.5 - # via azure-functions -wrapt==1.17.3 - # via - # opentelemetry-instrumentation - # opentelemetry-instrumentation-dbapi - # opentelemetry-instrumentation-urllib3 -yarl==1.22.0 - # via aiohttp -zipp==3.23.0 - # via importlib-metadata diff --git a/app/scoring_guide.md b/app/scoring_guide.md deleted file mode 100644 index 520b800..0000000 --- a/app/scoring_guide.md +++ /dev/null @@ -1,156 +0,0 @@ -# RFP Evaluation Scoring Guide - -## Overview -This guide defines the criteria for evaluating vendor proposals against RFP requirements. -The Technical Evaluation section carries a total weight of 70% of the overall score. - ---- - -## Technical Evaluation Criteria (70% Weight) - -### I-1. Agency Reputation (20 points, Weight: 14%) -Evaluates the agency, the team dedicated for the Work, and Reference partners. - -**Criteria:** -- Agency's industry reputation and standing -- Quality and experience of the dedicated project team -- Strength of reference partners and collaborations -- Track record with similar projects -- Client testimonials and references - -**Scoring:** -- 17-20: Exceptional reputation with outstanding team and references -- 13-16: Strong reputation with experienced team and good references -- 9-12: Adequate reputation with competent team -- 5-8: Limited reputation or team experience -- 0-4: Poor or unknown reputation - ---- - -### I-2. Methodology (20 points, Weight: 14%) -Evaluates methodology on the approach and the delivery timeline. - -**Criteria:** -- Clarity and feasibility of proposed approach -- Alignment with project objectives -- Realistic and well-structured delivery timeline -- Risk mitigation strategies -- Quality assurance processes - -**Scoring:** -- 17-20: Comprehensive methodology with excellent timeline management -- 13-16: Clear methodology with realistic timeline -- 9-12: Acceptable methodology with reasonable timeline -- 5-8: Vague methodology or unrealistic timeline -- 0-4: No clear methodology or timeline - ---- - -### I-3. Themes (20 points, Weight: 14%) -Evaluates the list of proposed themes. - -**Criteria:** -- Creativity and originality of proposed themes -- Relevance to project objectives -- Alignment with brand/company values -- Innovation and fresh perspectives -- Coherence across all themes - -**Scoring:** -- 17-20: Highly creative and perfectly aligned themes -- 13-16: Strong themes with good alignment -- 9-12: Adequate themes meeting basic requirements -- 5-8: Generic themes with limited creativity -- 0-4: Irrelevant or missing themes - ---- - -### I-4. Structure (20 points, Weight: 14%) -Evaluates the structure of the proposed contents according to best practices. - -**Criteria:** -- Logical organization of content -- Adherence to industry best practices -- Clear hierarchy and flow -- Completeness of content structure -- Professional presentation standards - -**Scoring:** -- 17-20: Exemplary structure following all best practices -- 13-16: Well-organized structure with minor gaps -- 9-12: Acceptable structure with some improvements needed -- 5-8: Disorganized or incomplete structure -- 0-4: No clear structure or major deficiencies - ---- - -### I-5. Examples (20 points, Weight: 14%) -Evaluates examples of recent work done. - -**Criteria:** -- Quality of work samples provided -- Relevance to current project requirements -- Recency of examples (within last 2-3 years) -- Diversity of portfolio -- Demonstrated capabilities and outcomes - -**Scoring:** -- 17-20: Outstanding portfolio with highly relevant recent work -- 13-16: Strong examples demonstrating clear capabilities -- 9-12: Adequate examples with moderate relevance -- 5-8: Limited or outdated examples -- 0-4: No examples or irrelevant samples - ---- - -## Score Calculation - -### Requirement Score (Technical) -- Sum of all individual requirement scores -- Maximum possible: 100 points - -### Weighted Score -Each requirement's weighted score = (Score / Maximum Score) ร— Weight - -**Example:** -- Agency Reputation: Score 14/20 ร— 14% = 9.8 weighted points -- Methodology: Score 16.67/20 ร— 14% = 11.67 weighted points - -### Composite Score -- Technical Weighted Score (70%) + Commercial Score (30%) -- Used for final ranking - ---- - -## Overall Score Interpretation - -| Weighted Score | Rating | Recommendation | -|----------------|--------|----------------| -| 60-70 | Excellent | Strongly recommended for selection | -| 50-59 | Very Good | Recommended with minor clarifications | -| 40-49 | Good | Consider with some negotiation | -| 30-39 | Acceptable | Review concerns before proceeding | -| Below 30 | Poor | Not recommended | - ---- - -## Output Format - -The evaluation should provide: -1. **Individual Requirement Scores** - Score for each of the 5 criteria (0-20) -2. **Requirement Score** - Total technical score (0-100) -3. **Weighted Scores** - Each criterion weighted by 14% -4. **Total Weighted Score** - Sum of weighted scores (max 70%) -5. **Strengths** - Key strengths identified -6. **Weaknesses** - Areas for improvement -7. **Recommendations** - Actionable suggestions - ---- - -## Additional Notes - -- All scores should be justified with specific references to the proposal -- Consider both explicit and implicit compliance with requirements -- Document any assumptions made during evaluation -- Flag any areas requiring clarification from the vendor -- Compare against RFP requirements point by point diff --git a/app/services/__init__.py b/app/services/__init__.py deleted file mode 100644 index 4ec4393..0000000 --- a/app/services/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Services package for RFP Analyzer.""" diff --git a/app/services/comparison_agent.py b/app/services/comparison_agent.py deleted file mode 100644 index 0730a3d..0000000 --- a/app/services/comparison_agent.py +++ /dev/null @@ -1,833 +0,0 @@ -""" -Comparison Agent for Multi-Vendor RFP Evaluation. - -This module provides an agent that compares evaluation results -across multiple vendors and generates comparison reports. -""" - -import os -import json -import time -import csv -import io -from datetime import datetime -from typing import List, Dict, Any, Optional, Callable - -from agent_framework.azure import AzureOpenAIResponsesClient -from azure.identity import DefaultAzureCredential -from pydantic import BaseModel, Field -from dotenv import load_dotenv - -from .logging_config import get_logger - -# Optional dependency for Word document generation -try: - from docx import Document - from docx.shared import Inches, Pt - from docx.enum.text import WD_ALIGN_PARAGRAPH - DOCX_AVAILABLE = True -except ImportError: - DOCX_AVAILABLE = False - -load_dotenv() - -# Get logger from centralized config -logger = get_logger(__name__) - - -class VendorRanking(BaseModel): - """Ranking for a single vendor.""" - rank: int = Field(description="Rank position (1 = best)") - vendor_name: str = Field(description="Vendor name") - total_score: float = Field(description="Total weighted score") - grade: str = Field(description="Letter grade") - key_strengths: List[str] = Field(description="Top 3 strengths") - key_concerns: List[str] = Field(description="Top 3 concerns") - recommendation: str = Field(description="Brief recommendation") - - -class CriterionComparison(BaseModel): - """Comparison of vendors for a specific criterion.""" - criterion_id: str = Field(description="Criterion ID") - criterion_name: str = Field(description="Criterion name") - weight: float = Field(description="Criterion weight") - best_vendor: str = Field(description="Best performing vendor for this criterion") - worst_vendor: str = Field(description="Worst performing vendor for this criterion") - score_range: str = Field(description="Score range across vendors (e.g., '65-92')") - insights: str = Field(description="Key insights for this criterion") - - -class ComparisonResult(BaseModel): - """Complete comparison result for multiple vendors.""" - rfp_title: str = Field(description="Title of the RFP") - comparison_date: str = Field(description="Date of comparison") - total_vendors: int = Field(description="Number of vendors compared") - - # Rankings - vendor_rankings: List[VendorRanking] = Field(description="Vendors ranked by score") - - # Criterion-level comparison - criterion_comparisons: List[CriterionComparison] = Field(description="Comparison by criterion") - - # Overall analysis - winner_summary: str = Field(description="Summary of recommended vendor") - comparison_insights: List[str] = Field(description="Key insights from comparison") - selection_recommendation: str = Field(description="Final selection recommendation") - risk_comparison: str = Field(description="Comparative risk assessment") - - -class ComparisonAgent: - """ - Agent for comparing multiple vendor evaluations. - - Takes evaluation results from multiple vendors and generates - comparative analysis, rankings, and recommendations. - """ - - SYSTEM_INSTRUCTIONS = """You are an expert procurement analyst specializing in vendor comparison and selection. - -Your task is to analyze evaluation results from multiple vendors responding to the same RFP -and provide a comprehensive comparative analysis. - -## YOUR RESPONSIBILITIES: - -1. **Rank Vendors**: Order vendors by total score, identifying the best performer -2. **Compare by Criterion**: Analyze how vendors performed on each evaluation criterion -3. **Identify Patterns**: Find strengths and weaknesses across the vendor pool -4. **Provide Insights**: Offer actionable insights for the selection committee -5. **Make Recommendations**: Provide clear selection recommendations - -## ANALYSIS APPROACH: - -For each vendor: -- Review their total score and individual criterion scores -- Identify their top 3 strengths and top 3 concerns -- Assess their suitability for the project - -For the comparison: -- Identify which vendors excel in which areas -- Note any significant score gaps between vendors -- Highlight criteria where all vendors performed well or poorly -- Consider risk factors and value for money - -## OUTPUT FORMAT: - -Respond with a valid JSON object: - -```json -{ - "rfp_title": "RFP title", - "comparison_date": "YYYY-MM-DD", - "total_vendors": , - "vendor_rankings": [ - { - "rank": 1, - "vendor_name": "Vendor Name", - "total_score": 85.5, - "grade": "B", - "key_strengths": ["strength 1", "strength 2", "strength 3"], - "key_concerns": ["concern 1", "concern 2", "concern 3"], - "recommendation": "Brief recommendation for this vendor" - } - ], - "criterion_comparisons": [ - { - "criterion_id": "C-1", - "criterion_name": "Criterion Name", - "weight": 20.0, - "best_vendor": "Best Vendor", - "worst_vendor": "Worst Vendor", - "score_range": "65-92", - "insights": "Key insight for this criterion" - } - ], - "winner_summary": "Summary of why the top vendor is recommended", - "comparison_insights": [ - "Key insight 1", - "Key insight 2", - "Key insight 3" - ], - "selection_recommendation": "Clear final recommendation with justification", - "risk_comparison": "Comparative risk assessment across vendors" -} -``` - -## IMPORTANT: -- Rank ALL vendors by score -- Compare ALL criteria -- Be objective and fair -- Support recommendations with evidence -- Respond with ONLY valid JSON""" - - def __init__(self): - """Initialize the comparison agent.""" - logger.info("Initializing ComparisonAgent...") - - self._validate_config() - - endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") - deployment_name = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME") - - self.client = AzureOpenAIResponsesClient( - credential=DefaultAzureCredential(), - endpoint=endpoint, - deployment_name=deployment_name, - api_version="v1" - ) - - self.deployment_name = deployment_name - logger.info("ComparisonAgent initialized") - - def _validate_config(self): - """Validate required configuration.""" - required_vars = ["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOYMENT_NAME"] - missing = [var for var in required_vars if not os.getenv(var)] - if missing: - raise ValueError(f"Missing required environment variables: {', '.join(missing)}") - - async def compare_evaluations( - self, - evaluations: List[Dict[str, Any]], - rfp_title: str, - progress_callback: Optional[Callable[[str], None]] = None, - reasoning_effort: str = "high" - ) -> Dict[str, Any]: - """ - Compare multiple vendor evaluations. - - Args: - evaluations: List of evaluation results from individual vendor scoring - rfp_title: Title of the RFP - progress_callback: Optional callback for progress updates - reasoning_effort: Reasoning effort level ("low", "medium", "high") - - Returns: - Dictionary containing comparison results - """ - start_time = time.time() - logger.info("Starting comparison of %d vendor evaluations (effort: %s)...", - len(evaluations), reasoning_effort) - - if progress_callback: - progress_callback("Preparing vendor comparison...") - - # Format evaluations for the prompt - evaluations_summary = self._format_evaluations_for_prompt(evaluations) - - user_prompt = f"""Please compare the following vendor evaluations and provide a comprehensive analysis. - -## RFP TITLE: {rfp_title} - -## VENDOR EVALUATIONS: - -{evaluations_summary} - ---- - -REQUIREMENTS: -1. Rank all vendors by total score -2. Compare performance on each criterion -3. Identify the best and worst performers per criterion -4. Provide clear selection recommendations -5. Assess comparative risks - -Respond with ONLY valid JSON matching the schema in your instructions.""" - - try: - agent = self.client.create_agent( - instructions=self.SYSTEM_INSTRUCTIONS, - name="Comparison Agent", - additional_chat_options={"reasoning": {"effort": reasoning_effort, "summary": "detailed"}} - ) - - if progress_callback: - progress_callback("Analyzing vendor comparisons...") - - result = await agent.run(user_prompt) - response_text = result.text - - # Log token usage - usage = result.usage_details - if usage: - logger.info("Comparison - Tokens: Input=%d, Output=%d, Total=%d", - usage.input_token_count or 0, - usage.output_token_count or 0, - usage.total_token_count or 0) - - # Parse the response - comparison_data = self._parse_response(response_text) - - duration = time.time() - start_time - logger.info("Comparison completed in %.2fs", duration) - - # Add metadata - comparison_data["_metadata"] = { - "comparison_timestamp": datetime.now().isoformat(), - "total_duration_seconds": round(duration, 2), - "vendors_compared": len(evaluations), - "model_deployment": self.deployment_name, - "reasoning_effort": reasoning_effort - } - - return comparison_data - - except Exception as e: - logger.error("Comparison failed: %s", str(e)) - raise - - def _format_evaluations_for_prompt(self, evaluations: List[Dict[str, Any]]) -> str: - """Format evaluations for the comparison prompt.""" - formatted = [] - - for i, eval_result in enumerate(evaluations, 1): - vendor_name = eval_result.get("supplier_name", f"Vendor {i}") - total_score = eval_result.get("total_score", 0) - grade = eval_result.get("grade", "N/A") - - summary = f"""### Vendor {i}: {vendor_name} -- **Total Score:** {total_score:.2f} -- **Grade:** {grade} - -**Criterion Scores:** -""" - - # Add criterion scores - criterion_scores = eval_result.get("criterion_scores", []) - for cs in criterion_scores: - summary += f"- {cs.get('criterion_name', cs.get('criterion_id', 'Unknown'))}: {cs.get('raw_score', 0):.1f} (weighted: {cs.get('weighted_score', 0):.2f})\n" - - # Add strengths and weaknesses - strengths = eval_result.get("overall_strengths", []) - weaknesses = eval_result.get("overall_weaknesses", []) - - if strengths: - summary += "\n**Strengths:** " + ", ".join(strengths[:5]) - if weaknesses: - summary += "\n**Weaknesses:** " + ", ".join(weaknesses[:5]) - - formatted.append(summary) - - return "\n\n---\n\n".join(formatted) - - def _parse_response(self, response_text: str) -> Dict[str, Any]: - """Parse the agent response.""" - text = response_text.strip() - - # Remove markdown code blocks - if text.startswith("```json"): - text = text[7:] - elif text.startswith("```"): - text = text[3:] - if text.endswith("```"): - text = text[:-3] - text = text.strip() - - try: - return json.loads(text) - except json.JSONDecodeError as e: - logger.error("Failed to parse comparison JSON: %s", str(e)) - return { - "rfp_title": "Unknown RFP", - "comparison_date": datetime.now().strftime("%Y-%m-%d"), - "total_vendors": 0, - "vendor_rankings": [], - "criterion_comparisons": [], - "winner_summary": "Error parsing comparison results", - "comparison_insights": [], - "selection_recommendation": "Unable to provide recommendation due to parsing error", - "risk_comparison": "Unable to assess" - } - - def generate_csv_report(self, comparison: Dict[str, Any], evaluations: List[Dict[str, Any]]) -> str: - """ - Generate a CSV report from comparison results. - - Args: - comparison: Comparison result from compare_evaluations - evaluations: Original evaluation results - - Returns: - CSV content as string - """ - output = io.StringIO() - - # Create comprehensive CSV with all metrics - - # Section 1: Summary Rankings - writer = csv.writer(output) - writer.writerow(["RFP Comparison Report"]) - writer.writerow(["RFP Title", comparison.get("rfp_title", "")]) - writer.writerow(["Comparison Date", comparison.get("comparison_date", "")]) - writer.writerow(["Total Vendors", comparison.get("total_vendors", len(evaluations))]) - writer.writerow([]) - - # Vendor Rankings - writer.writerow(["=== VENDOR RANKINGS ==="]) - writer.writerow(["Rank", "Vendor Name", "Total Score", "Grade", "Recommendation"]) - - for ranking in comparison.get("vendor_rankings", []): - writer.writerow([ - ranking.get("rank", ""), - ranking.get("vendor_name", ""), - f"{ranking.get('total_score', 0):.2f}", - ranking.get("grade", ""), - ranking.get("recommendation", "") - ]) - - writer.writerow([]) - - # Criterion Comparison Matrix - writer.writerow(["=== CRITERION COMPARISON ==="]) - - # Get all criteria from first evaluation - all_criteria = [] - if evaluations: - all_criteria = [cs.get("criterion_name", cs.get("criterion_id", "Unknown")) - for cs in evaluations[0].get("criterion_scores", [])] - - # Header row - header = ["Criterion", "Weight"] + [e.get("supplier_name", f"Vendor {i+1}") - for i, e in enumerate(evaluations)] - writer.writerow(header) - - # Score rows for each criterion - for criterion_idx, criterion_name in enumerate(all_criteria): - row = [criterion_name] - - # Get weight from first evaluation - if evaluations and criterion_idx < len(evaluations[0].get("criterion_scores", [])): - weight = evaluations[0]["criterion_scores"][criterion_idx].get("weight", 0) - row.append(f"{weight:.1f}%") - else: - row.append("") - - # Add each vendor's score for this criterion - for eval_result in evaluations: - scores = eval_result.get("criterion_scores", []) - if criterion_idx < len(scores): - score = scores[criterion_idx].get("raw_score", 0) - row.append(f"{score:.1f}") - else: - row.append("") - - writer.writerow(row) - - # Total scores row - total_row = ["TOTAL SCORE", "100%"] - for eval_result in evaluations: - total_row.append(f"{eval_result.get('total_score', 0):.2f}") - writer.writerow(total_row) - - writer.writerow([]) - - # Insights - writer.writerow(["=== KEY INSIGHTS ==="]) - for insight in comparison.get("comparison_insights", []): - writer.writerow([insight]) - - writer.writerow([]) - writer.writerow(["=== SELECTION RECOMMENDATION ==="]) - writer.writerow([comparison.get("selection_recommendation", "")]) - - return output.getvalue() - - -def generate_word_report(evaluation: Dict[str, Any], rfp_content: str = "") -> bytes: - """ - Generate a Word document report from evaluation results. - - Args: - evaluation: Evaluation result dictionary - rfp_content: Optional RFP content for context - - Returns: - Word document as bytes - """ - if not DOCX_AVAILABLE: - logger.warning("python-docx not installed. Word export not available.") - return None - - doc = Document() - - # Title - title = doc.add_heading('RFP Evaluation Report', 0) - title.alignment = WD_ALIGN_PARAGRAPH.CENTER - - # Summary section - doc.add_heading('Evaluation Summary', level=1) - - table = doc.add_table(rows=6, cols=2) - table.style = 'Table Grid' - - summary_data = [ - ("RFP Title", evaluation.get("rfp_title", "")), - ("Vendor Name", evaluation.get("supplier_name", "")), - ("Total Score", f"{evaluation.get('total_score', 0):.2f}"), - ("Grade", evaluation.get("grade", "")), - ("Evaluation Date", evaluation.get("evaluation_date", "")), - ("Recommendation", evaluation.get("recommendation", "")) - ] - - for i, (label, value) in enumerate(summary_data): - row = table.rows[i] - row.cells[0].text = label - row.cells[1].text = str(value) - - doc.add_paragraph() - - # Criterion Scores Overview - doc.add_heading('Criterion Scores Overview', level=1) - - criterion_scores = evaluation.get("criterion_scores", []) - if criterion_scores: - scores_table = doc.add_table(rows=len(criterion_scores) + 1, cols=4) - scores_table.style = 'Table Grid' - - # Header - headers = ["Criterion", "Weight", "Score", "Weighted Score"] - header_row = scores_table.rows[0] - for i, h in enumerate(headers): - header_row.cells[i].text = h - - # Data rows - for i, cs in enumerate(criterion_scores, 1): - row = scores_table.rows[i] - row.cells[0].text = cs.get("criterion_name", "") - row.cells[1].text = f"{cs.get('weight', 0):.1f}%" - row.cells[2].text = f"{cs.get('raw_score', 0):.1f}" - row.cells[3].text = f"{cs.get('weighted_score', 0):.2f}" - - doc.add_paragraph() - - # Detailed Criterion Analysis with Justifications - doc.add_heading('Detailed Criterion Analysis', level=1) - - for cs in criterion_scores: - criterion_name = cs.get("criterion_name", "Unknown") - score = cs.get("raw_score", 0) - weight = cs.get("weight", 0) - justification = cs.get("justification", "") - strengths = cs.get("strengths", []) - gaps = cs.get("gaps", []) - - # Criterion header - doc.add_heading(f'{criterion_name} (Score: {score:.1f}/100)', level=2) - doc.add_paragraph(f"Weight: {weight:.1f}%") - - # Justification - if justification: - doc.add_heading('Score Justification', level=3) - doc.add_paragraph(justification) - - # Strengths - if strengths: - doc.add_heading('Strengths', level=3) - for s in strengths: - doc.add_paragraph(f"โ€ข {s}", style='List Bullet') - - # Gaps - if gaps: - doc.add_heading('Gaps/Areas for Improvement', level=3) - for g in gaps: - doc.add_paragraph(f"โ€ข {g}", style='List Bullet') - - doc.add_paragraph() - - # Overall Strengths - doc.add_heading('Overall Key Strengths', level=1) - for strength in evaluation.get("overall_strengths", []): - doc.add_paragraph(f"โ€ข {strength}", style='List Bullet') - - # Overall Weaknesses - doc.add_heading('Overall Key Weaknesses', level=1) - for weakness in evaluation.get("overall_weaknesses", []): - doc.add_paragraph(f"โ€ข {weakness}", style='List Bullet') - - # Recommendations - doc.add_heading('Recommendations', level=1) - for rec in evaluation.get("recommendations", []): - doc.add_paragraph(f"โ€ข {rec}", style='List Bullet') - - # Executive Summary - doc.add_heading('Executive Summary', level=1) - doc.add_paragraph(evaluation.get("executive_summary", "")) - - # Risk Assessment - doc.add_heading('Risk Assessment', level=1) - doc.add_paragraph(evaluation.get("risk_assessment", "")) - - # Save to bytes - buffer = io.BytesIO() - doc.save(buffer) - buffer.seek(0) - return buffer.getvalue() - - -def generate_full_analysis_report(comparison: Dict[str, Any], evaluations: List[Dict[str, Any]]) -> bytes: - """ - Generate a comprehensive Word document with the full analysis report including comparison. - - Args: - comparison: Comparison result dictionary - evaluations: List of evaluation result dictionaries - - Returns: - Word document as bytes - """ - if not DOCX_AVAILABLE: - logger.warning("python-docx not installed. Word export not available.") - return None - - doc = Document() - - # Title - title = doc.add_heading('RFP Vendor Analysis Report', 0) - title.alignment = WD_ALIGN_PARAGRAPH.CENTER - - # Report metadata - doc.add_paragraph(f"Report Date: {comparison.get('comparison_date', datetime.now().strftime('%Y-%m-%d'))}") - doc.add_paragraph(f"RFP Title: {comparison.get('rfp_title', 'N/A')}") - doc.add_paragraph(f"Vendors Evaluated: {comparison.get('total_vendors', len(evaluations))}") - doc.add_paragraph() - - # ========================================== - # SECTION 1: EXECUTIVE SUMMARY - # ========================================== - doc.add_heading('Executive Summary', level=1) - - # Selection Recommendation - recommendation = comparison.get("selection_recommendation", "") - if recommendation: - doc.add_heading('Selection Recommendation', level=2) - doc.add_paragraph(recommendation) - - # Winner Summary - winner_summary = comparison.get("winner_summary", "") - if winner_summary: - doc.add_heading('Winner Summary', level=2) - doc.add_paragraph(winner_summary) - - # Key Insights - insights = comparison.get("comparison_insights", []) - if insights: - doc.add_heading('Key Insights', level=2) - for insight in insights: - doc.add_paragraph(f"โ€ข {insight}", style='List Bullet') - - doc.add_page_break() - - # ========================================== - # SECTION 2: VENDOR RANKINGS - # ========================================== - doc.add_heading('Vendor Rankings', level=1) - - rankings = comparison.get("vendor_rankings", []) - if rankings: - # Rankings table - rankings_table = doc.add_table(rows=len(rankings) + 1, cols=4) - rankings_table.style = 'Table Grid' - - headers = ["Rank", "Vendor", "Score", "Grade"] - header_row = rankings_table.rows[0] - for i, h in enumerate(headers): - header_row.cells[i].text = h - - for i, ranking in enumerate(rankings, 1): - row = rankings_table.rows[i] - row.cells[0].text = str(ranking.get("rank", i)) - row.cells[1].text = ranking.get("vendor_name", "") - row.cells[2].text = f"{ranking.get('total_score', 0):.1f}" - row.cells[3].text = ranking.get("grade", "") - - doc.add_paragraph() - - # Detailed ranking information - for ranking in rankings: - vendor_name = ranking.get("vendor_name", "Unknown") - rank = ranking.get("rank", 0) - - doc.add_heading(f'#{rank} - {vendor_name}', level=2) - - # Key Strengths - strengths = ranking.get("key_strengths", []) - if strengths: - doc.add_heading('Key Strengths', level=3) - for s in strengths: - doc.add_paragraph(f"โ€ข {s}", style='List Bullet') - - # Key Concerns - concerns = ranking.get("key_concerns", []) - if concerns: - doc.add_heading('Key Concerns', level=3) - for c in concerns: - doc.add_paragraph(f"โ€ข {c}", style='List Bullet') - - # Recommendation - rec = ranking.get("recommendation", "") - if rec: - doc.add_paragraph(f"Recommendation: {rec}") - - doc.add_paragraph() - - doc.add_page_break() - - # ========================================== - # SECTION 3: CRITERION COMPARISON - # ========================================== - doc.add_heading('Performance by Criterion', level=1) - - criterion_comparisons = comparison.get("criterion_comparisons", []) - if criterion_comparisons: - for cc in criterion_comparisons: - criterion_name = cc.get("criterion_name", "Unknown") - weight = cc.get("weight", 0) - - doc.add_heading(f'{criterion_name} (Weight: {weight:.1f}%)', level=2) - - # Summary table - cc_table = doc.add_table(rows=3, cols=2) - cc_table.style = 'Table Grid' - cc_table.rows[0].cells[0].text = "Best Performer" - cc_table.rows[0].cells[1].text = cc.get("best_vendor", "N/A") - cc_table.rows[1].cells[0].text = "Lowest Performer" - cc_table.rows[1].cells[1].text = cc.get("worst_vendor", "N/A") - cc_table.rows[2].cells[0].text = "Score Range" - cc_table.rows[2].cells[1].text = cc.get("score_range", "N/A") - - insights = cc.get("insights", "") - if insights: - doc.add_paragraph() - doc.add_paragraph(f"Insights: {insights}") - - doc.add_paragraph() - - # Score comparison table - doc.add_heading('Score Comparison Matrix', level=2) - - if evaluations: - # Get all criteria - all_criteria = [] - if evaluations[0].get("criterion_scores"): - all_criteria = evaluations[0]["criterion_scores"] - - if all_criteria: - # Create comparison table - num_vendors = len(evaluations) - matrix_table = doc.add_table(rows=len(all_criteria) + 2, cols=num_vendors + 2) - matrix_table.style = 'Table Grid' - - # Header row - matrix_table.rows[0].cells[0].text = "Criterion" - matrix_table.rows[0].cells[1].text = "Weight" - for i, eval_result in enumerate(evaluations): - matrix_table.rows[0].cells[i + 2].text = eval_result.get("supplier_name", f"Vendor {i+1}")[:15] - - # Data rows - for row_idx, criterion in enumerate(all_criteria, 1): - matrix_table.rows[row_idx].cells[0].text = criterion.get("criterion_name", "") - matrix_table.rows[row_idx].cells[1].text = f"{criterion.get('weight', 0):.1f}%" - - for col_idx, eval_result in enumerate(evaluations): - scores = eval_result.get("criterion_scores", []) - if row_idx - 1 < len(scores): - score = scores[row_idx - 1].get("raw_score", 0) - matrix_table.rows[row_idx].cells[col_idx + 2].text = f"{score:.1f}" - - # Total row - last_row = len(all_criteria) + 1 - matrix_table.rows[last_row].cells[0].text = "TOTAL SCORE" - matrix_table.rows[last_row].cells[1].text = "100%" - for col_idx, eval_result in enumerate(evaluations): - matrix_table.rows[last_row].cells[col_idx + 2].text = f"{eval_result.get('total_score', 0):.1f}" - - doc.add_page_break() - - # ========================================== - # SECTION 4: RISK ASSESSMENT - # ========================================== - doc.add_heading('Risk Assessment', level=1) - - risk_comparison = comparison.get("risk_comparison", "") - if risk_comparison: - doc.add_paragraph(risk_comparison) - else: - doc.add_paragraph("No specific risk assessment provided.") - - doc.add_page_break() - - # ========================================== - # SECTION 5: DETAILED VENDOR REPORTS - # ========================================== - doc.add_heading('Detailed Vendor Reports', level=1) - - for eval_result in evaluations: - vendor_name = eval_result.get("supplier_name", "Unknown") - total_score = eval_result.get("total_score", 0) - grade = eval_result.get("grade", "N/A") - - doc.add_heading(f'{vendor_name}', level=2) - - # Summary - doc.add_paragraph(f"Total Score: {total_score:.1f} | Grade: {grade}") - - # Executive Summary - exec_summary = eval_result.get("executive_summary", "") - if exec_summary: - doc.add_heading('Executive Summary', level=3) - doc.add_paragraph(exec_summary) - - # Criterion Scores with Justifications - doc.add_heading('Criterion Analysis', level=3) - - criterion_scores = eval_result.get("criterion_scores", []) - for cs in criterion_scores: - criterion_name = cs.get("criterion_name", "Unknown") - score = cs.get("raw_score", 0) - weight = cs.get("weight", 0) - justification = cs.get("justification", "") - strengths = cs.get("strengths", []) - gaps = cs.get("gaps", []) - - doc.add_heading(f'{criterion_name}: {score:.1f}/100 (Weight: {weight:.1f}%)', level=4) - - if justification: - doc.add_paragraph(f"Justification: {justification}") - - if strengths: - doc.add_paragraph("Strengths:") - for s in strengths: - doc.add_paragraph(f" โ€ข {s}") - - if gaps: - doc.add_paragraph("Gaps:") - for g in gaps: - doc.add_paragraph(f" โ€ข {g}") - - # Overall Strengths - overall_strengths = eval_result.get("overall_strengths", []) - if overall_strengths: - doc.add_heading('Overall Strengths', level=3) - for s in overall_strengths: - doc.add_paragraph(f"โ€ข {s}", style='List Bullet') - - # Overall Weaknesses - overall_weaknesses = eval_result.get("overall_weaknesses", []) - if overall_weaknesses: - doc.add_heading('Overall Weaknesses', level=3) - for w in overall_weaknesses: - doc.add_paragraph(f"โ€ข {w}", style='List Bullet') - - # Recommendations - recommendations = eval_result.get("recommendations", []) - if recommendations: - doc.add_heading('Recommendations', level=3) - for rec in recommendations: - doc.add_paragraph(f"โ€ข {rec}", style='List Bullet') - - doc.add_page_break() - - # Save to bytes - buffer = io.BytesIO() - doc.save(buffer) - buffer.seek(0) - return buffer.getvalue() diff --git a/app/services/content_understanding_client.py b/app/services/content_understanding_client.py deleted file mode 100644 index 4c3432e..0000000 --- a/app/services/content_understanding_client.py +++ /dev/null @@ -1,1008 +0,0 @@ -import base64 -import json -import os -import requests -import time - -from dataclasses import dataclass -from datetime import datetime, timedelta, timezone -from requests.models import Response -from typing import Any, Dict, List, Optional -from pathlib import Path - -from azure.identity import DefaultAzureCredential -from azure.storage.blob import ( - BlobServiceClient, - generate_container_sas, - ContainerSasPermissions -) -from azure.storage.blob.aio import ContainerClient - -from .logging_config import get_logger - - -POLL_TIMEOUT_SECONDS = 1200 # 20 minutes - - -@dataclass -class ReferenceDocItem: - filename: str - file_path: str - result_file_name: str - result_file_path: Optional[str] = None - - -class AzureContentUnderstandingClient: - - PREBUILT_DOCUMENT_ANALYZER_ID: str = "prebuilt-documentSearch" - OCR_RESULT_FILE_SUFFIX: str = ".result.json" - LABEL_FILE_SUFFIX: str = ".labels.json" - KNOWLEDGE_SOURCE_LIST_FILE_NAME: str = "sources.jsonl" - SAS_EXPIRY_HOURS: int = 1 - - # https://learn.microsoft.com/en-us/azure/ai-services/content-understanding/service-limits#document-and-text - SUPPORTED_FILE_TYPES_DOCUMENT_TXT: List[str] = [ - ".pdf", - ".tiff", - ".jpg", - ".jpeg", - ".png", - ".bmp", - ".heif", - ".docx", - ".xlsx", - ".pptx", - ".txt", - ".html", - ".md", - ".eml", - ".msg", - ".xml", - ] - - SUPPORTED_FILE_TYPES_DOCUMENT: List[str] = [ - ".pdf", - ".tiff", - ".jpg", - ".jpeg", - ".png", - ".bmp", - ".heif", - ] # Pro mode and Training for Standard mode only support document data - - # Maximum number of pages to retrieve when following pagination links - MAX_PAGINATION_PAGES: int = 1000 - - def __init__( - self, - endpoint: str, - api_version: str, - subscription_key: str = None, - token_provider: callable = None, - x_ms_useragent: str = "cu-sample-code", - ): - if not subscription_key and not token_provider: - raise ValueError( - "Either subscription key or token provider must be provided." - ) - if not api_version: - raise ValueError("API version must be provided.") - if not endpoint: - raise ValueError("Endpoint must be provided.") - - self._endpoint = endpoint.rstrip("/") - self._api_version = api_version - self._logger = get_logger(__name__) - - token = token_provider() if token_provider else None - - self._headers = self._get_headers(subscription_key, token, x_ms_useragent) - - def _get_analyzer_url(self, endpoint: str, api_version: str, analyzer_id: str) -> str: - return f"{endpoint}/contentunderstanding/analyzers/{analyzer_id}?api-version={api_version}" # noqa - - def _get_analyzer_list_url(self, endpoint: str, api_version: str) -> str: - return f"{endpoint}/contentunderstanding/analyzers?api-version={api_version}" - - def _get_analyze_url(self, endpoint: str, api_version: str, analyzer_id: str) -> str: - return f"{endpoint}/contentunderstanding/analyzers/{analyzer_id}:analyze?api-version={api_version}" # noqa - - def _get_analyze_binary_url(self, endpoint: str, api_version: str, analyzer_id: str) -> str: - return f"{endpoint}/contentunderstanding/analyzers/{analyzer_id}:analyzeBinary?api-version={api_version}" # noqa - - def _get_training_data_config( - self, storage_container_sas_url: str, storage_container_path_prefix: str - ) -> Dict[str, str]: - return { - "containerUrl": storage_container_sas_url, - "kind": "blob", - "prefix": storage_container_path_prefix, - } - - def _get_pro_mode_reference_docs_config( - self, storage_container_sas_url: str, storage_container_path_prefix: str - ) -> List[Dict[str, str]]: - return [{ - "kind": "reference", - "containerUrl": storage_container_sas_url, - "prefix": storage_container_path_prefix, - "fileListPath": self.KNOWLEDGE_SOURCE_LIST_FILE_NAME, - }] - - def _get_classifier_url(self, endpoint: str, api_version: str, classifier_id: str) -> str: - return f"{endpoint}/contentunderstanding/classifiers/{classifier_id}?api-version={api_version}" - - def _get_classify_url(self, endpoint: str, api_version: str, classifier_id: str) -> str: - return f"{endpoint}/contentunderstanding/classifiers/{classifier_id}:classify?api-version={api_version}" - - def _get_defaults_url(self, endpoint: str, api_version: str) -> str: - return f"{endpoint}/contentunderstanding/defaults?api-version={api_version}" - - def _get_headers( - self, subscription_key: str, api_token: str, x_ms_useragent: str - ) -> Dict[str, str]: - """Returns the headers for the HTTP requests. - Args: - subscription_key (str): The subscription key for the service. - api_token (str): The API token for the service. - enable_face_identification (bool): A flag to enable face identification. - Returns: - dict: A dictionary containing the headers for the HTTP requests. - """ - headers = ( - {"Ocp-Apim-Subscription-Key": subscription_key} - if subscription_key - else {"Authorization": f"Bearer {api_token}"} - ) - headers["x-ms-useragent"] = x_ms_useragent - return headers - - def _raise_for_status_with_detail(self, response: Response) -> None: - """ - Raises HTTPError with detailed error information from the response. - - Args: - response: The HTTP response object to check - - Raises: - requests.exceptions.HTTPError: If the response status indicates an error, - with additional context from the response body - """ - if response.ok: - return - - try: - # Try to extract error details from response body - error_detail = "" - try: - error_json = response.json() - if "error" in error_json: - error_info = error_json["error"] - error_code = error_info.get("code", "Unknown") - error_message = error_info.get("message", "No message provided") - error_detail = f"\n Error Code: {error_code}\n Error Message: {error_message}" - - # Include additional details if available - if "details" in error_info: - error_detail += f"\n Details: {error_info['details']}" - if "innererror" in error_info: - error_detail += f"\n Inner Error: {error_info['innererror']}" - else: - error_detail = f"\n Response Body: {json.dumps(error_json, indent=2)}" - except (ValueError, json.JSONDecodeError): - # If response is not JSON, include raw text - if response.text: - error_detail = f"\n Response Text: {response.text[:500]}" - except Exception: - # If anything goes wrong parsing the error, just continue with basic error - error_detail = "" - - # Create detailed error message - error_msg = f"{response.status_code} {response.reason} for url: {response.url}{error_detail}" - - # Raise HTTPError with the detailed message - http_error = requests.exceptions.HTTPError(error_msg, response=response) - raise http_error - - @staticmethod - def is_supported_doc_type_by_file_ext(file_ext: str, is_document: bool=False) -> bool: - """ - Checks if the given file extension is supported. - - Args: - file_ext (str): The file extension to check. - is_document (bool): If True, checks against Document supported file types. - - Returns: - bool: True if the file type is supported, False otherwise. - """ - supported_types = ( - AzureContentUnderstandingClient.SUPPORTED_FILE_TYPES_DOCUMENT - if is_document else AzureContentUnderstandingClient.SUPPORTED_FILE_TYPES_DOCUMENT_TXT - ) - return file_ext.lower() in supported_types - - @staticmethod - def is_supported_doc_type_by_file_path(file_path: Path, is_document: bool=False) -> bool: - """ - Checks if the given file path has a supported file type. - - Args: - file_path (Path): The local path to the file to check. - is_document (bool): If True, checks against Document supported file types. - - Returns: - bool: True if the file type is supported, False otherwise. - """ - if not file_path.is_file(): - return False - file_ext = file_path.suffix.lower() - return AzureContentUnderstandingClient.is_supported_doc_type_by_file_ext(file_ext, is_document) - - @staticmethod - def generate_temp_container_sas_url( - account_name: str, - container_name: str, - permissions: Optional[ContainerSasPermissions] = None, - expiry_hours: Optional[int] = None, - ) -> str: - """ - Generate a temporary SAS URL for an Azure Blob container using Azure AD authentication. - - Args: - account_name (str): The Azure Storage account name. - container_name (str): The name of the container. - permissions (ContainerSasPermissions, optional): Permissions to assign to the SAS token. - Defaults to read and list permissions. - expiry_hours (int, optional): Number of hours until the SAS token expires. - Defaults to `AzureContentUnderstandingClient.SAS_EXPIRY_HOURS`. - - Returns: - str: The SAS URL for the container. - """ - if permissions is None: - permissions = ContainerSasPermissions(read=True, list=True) - expiry_duration = timedelta(hours=expiry_hours or AzureContentUnderstandingClient.SAS_EXPIRY_HOURS) - - account_url = f"https://{account_name}.blob.core.windows.net" - blob_service_client = BlobServiceClient(account_url=account_url, credential=DefaultAzureCredential()) - - # Get user delegation key - start_time = datetime.now(timezone.utc) - expiry_time = start_time + expiry_duration - delegation_key = blob_service_client.get_user_delegation_key(start_time, expiry_time) - - sas_token = generate_container_sas( - account_name=account_name, - container_name=container_name, - user_delegation_key=delegation_key, - permission=permissions, - expiry=expiry_time, - start=start_time, - ) - - return f"{account_url}/{container_name}?{sas_token}" - - def get_all_analyzers(self) -> Dict[str, Any]: - """ - Retrieves a list of all available analyzers from the content understanding service. - - This method sends a GET request to the service endpoint to fetch the list of analyzers. - It automatically follows pagination links (nextLink) to retrieve all pages of results. - It raises an HTTPError if the request fails. - - Returns: - dict: A dictionary containing the JSON response from the service, which includes - the complete list of available analyzers across all pages in the "value" key. - - Raises: - requests.exceptions.HTTPError: If the HTTP request returned an unsuccessful status code. - RuntimeError: If too many pages are encountered (likely indicating a pagination loop). - ValueError: If the API response contains an invalid 'value' field (not a list). - """ - all_analyzers = [] - url = self._get_analyzer_list_url(self._endpoint, self._api_version) - visited_urls = set() - page_count = 0 - - while url: - # Prevent infinite loops from circular pagination links - if url in visited_urls: - raise RuntimeError(f"Circular pagination detected: {url} was already visited") - - visited_urls.add(url) - page_count += 1 - - # Check page count after incrementing to properly enforce limit - if page_count > self.MAX_PAGINATION_PAGES: - raise RuntimeError( - f"Maximum pagination limit ({self.MAX_PAGINATION_PAGES} pages) exceeded. " - f"This likely indicates a pagination loop or misconfiguration." - ) - - response = requests.get(url=url, headers=self._headers) - self._raise_for_status_with_detail(response) - response_json = response.json() - - # Collect analyzers from current page - analyzers = response_json.get("value", []) - if not isinstance(analyzers, list): - # Include structure info without potentially sensitive response content - structure_keys = list(response_json.keys()) if isinstance(response_json, dict) else [] - raise ValueError( - f"Expected 'value' to be a list, got {type(analyzers).__name__}. " - f"Response contains keys: {structure_keys}" - ) - all_analyzers.extend(analyzers) - - # Get the next page URL, if it exists - url = response_json.get("nextLink") - - # Return in the same format as the original response - return {"value": all_analyzers} - - def get_defaults(self) -> Dict[str, Any]: - """ - Retrieves the current default settings for the Content Understanding resource. - - This method sends a GET request to the service endpoint to fetch the default - model deployment mappings. - - Returns: - dict: A dictionary containing the default settings, including modelDeployments. - Example: {"modelDeployments": {"gpt-4.1": "myGpt41Deployment", ...}} - - Raises: - requests.exceptions.HTTPError: If the HTTP request returned an unsuccessful status code. - """ - response = requests.get( - url=self._get_defaults_url(self._endpoint, self._api_version), - headers=self._headers, - ) - self._raise_for_status_with_detail(response) - return response.json() - - def update_defaults(self, model_deployments: Dict[str, Optional[str]]) -> Dict[str, Any]: - """ - Updates the default model deployment mappings for the Content Understanding resource. - - This is a PATCH operation using application/merge-patch+json. You can update individual - model deployments without sending the entire object. Any keys you include will be - added/updated. You can remove keys by setting them to None. - - Args: - model_deployments (Dict[str, Optional[str]]): A dictionary mapping model names to - deployment names. Set a value to None to remove that mapping. - Example: {"gpt-4.1": "myGpt41Deployment", "gpt-4o": "eastus-gpt4o-deployment"} - - Returns: - dict: A dictionary containing the updated default settings. - - Raises: - requests.exceptions.HTTPError: If the HTTP request returned an unsuccessful status code. - - Example: - # Update specific deployments - client.update_defaults({ - "gpt-4o": "new-deployment-name", - "text-embedding-3-large": "myTextEmbedding3LargeDeployment" - }) - - # Remove a deployment mapping - client.update_defaults({"gpt-4.1": None}) - """ - headers = self._headers.copy() - headers["Content-Type"] = "application/merge-patch+json" - - body = {"modelDeployments": model_deployments} - - response = requests.patch( - url=self._get_defaults_url(self._endpoint, self._api_version), - headers=headers, - json=body, - ) - self._raise_for_status_with_detail(response) - return response.json() - - def get_analyzer_detail_by_id(self, analyzer_id: str) -> Dict[str, Any]: - """ - Retrieves a specific analyzer detail through analyzerid from the content understanding service. - This method sends a GET request to the service endpoint to get the analyzer detail. - - Args: - analyzer_id (str): The unique identifier for the analyzer. - - Returns: - dict: A dictionary containing the JSON response from the service, which includes the target analyzer detail. - - Raises: - HTTPError: If the request fails. - """ - response = requests.get( - url=self._get_analyzer_url(self._endpoint, self._api_version, analyzer_id), - headers=self._headers, - ) - self._raise_for_status_with_detail(response) - return response.json() - - def begin_create_analyzer( - self, - analyzer_id: str, - analyzer_template: dict = None, - analyzer_template_path: str = "", - training_storage_container_sas_url: str = "", - training_storage_container_path_prefix: str = "", - pro_mode_reference_docs_storage_container_sas_url: str = "", - pro_mode_reference_docs_storage_container_path_prefix: str = "", - ) -> Response: - """ - Initiates the creation of an analyzer with the given ID and schema. - - Args: - analyzer_id (str): The unique identifier for the analyzer. - analyzer_template (dict, optional): The schema definition for the analyzer. Defaults to None. - analyzer_template_path (str, optional): The file path to the analyzer schema JSON file. Defaults to "". - training_storage_container_sas_url (str, optional): The SAS URL for the training storage container. Defaults to "". - training_storage_container_path_prefix (str, optional): The path prefix within the training storage container. Defaults to "". - - Raises: - ValueError: If neither `analyzer_template` nor `analyzer_template_path` is provided. - requests.exceptions.HTTPError: If the HTTP request to create the analyzer fails. - - Returns: - requests.Response: The response object from the HTTP request. - """ - if analyzer_template_path and Path(analyzer_template_path).exists(): - with open(analyzer_template_path, "r") as file: - analyzer_template = json.load(file) - - if not analyzer_template: - raise ValueError("Analyzer schema must be provided.") - - if ( - training_storage_container_sas_url - and training_storage_container_path_prefix - ): # noqa - if not training_storage_container_path_prefix.endswith("/"): - training_storage_container_path_prefix += "/" - analyzer_template["trainingData"] = self._get_training_data_config( - training_storage_container_sas_url, - training_storage_container_path_prefix, - ) - - if ( - pro_mode_reference_docs_storage_container_sas_url - and pro_mode_reference_docs_storage_container_path_prefix - ): # noqa - if not pro_mode_reference_docs_storage_container_path_prefix.endswith("/"): - pro_mode_reference_docs_storage_container_path_prefix += "/" - analyzer_template["knowledgeSources"] = self._get_pro_mode_reference_docs_config( - pro_mode_reference_docs_storage_container_sas_url, - pro_mode_reference_docs_storage_container_path_prefix, - ) - - headers = {"Content-Type": "application/json"} - headers.update(self._headers) - - response = requests.put( - url=self._get_analyzer_url(self._endpoint, self._api_version, analyzer_id), - headers=headers, - json=analyzer_template, - ) - self._raise_for_status_with_detail(response) - self._logger.info(f"Analyzer {analyzer_id} create request accepted.") - return response - - def delete_analyzer(self, analyzer_id: str) -> Response: - """ - Deletes an analyzer with the specified analyzer ID. - - Args: - analyzer_id (str): The ID of the analyzer to be deleted. - - Returns: - response: The response object from the delete request. - - Raises: - HTTPError: If the delete request fails. - """ - response = requests.delete( - url=self._get_analyzer_url(self._endpoint, self._api_version, analyzer_id), - headers=self._headers, - ) - self._raise_for_status_with_detail(response) - self._logger.info(f"Deleting analyzer: {analyzer_id}") - return response - - def begin_analyze_url(self, analyzer_id: str, url: str) -> Response: - """ - Begins the analysis of a document from a URL using the specified analyzer. - Uses the :analyze endpoint for URL-based analysis. - - Args: - analyzer_id (str): The ID of the analyzer to use. - url (str): The URL of the document to analyze. - - Returns: - Response: The response from the analysis request. - - Raises: - ValueError: If the URL is not valid. - HTTPError: If the HTTP request returned an unsuccessful status code. - """ - if not (url.startswith("https://") or url.startswith("http://")): - raise ValueError("URL must start with http:// or https://") - - # URL must be wrapped in inputs array - data = {"inputs": [{"url": url}]} - headers = {"Content-Type": "application/json"} - headers.update(self._headers) - - response = requests.post( - url=self._get_analyze_url( - self._endpoint, self._api_version, analyzer_id - ), - headers=headers, - json=data, - ) - - self._raise_for_status_with_detail(response) - self._logger.info( - f"Analyzing URL {url} with analyzer: {analyzer_id}" - ) - return response - - def begin_analyze_binary_bytes(self, analyzer_id: str, file_bytes: bytes) -> Response: - """ - Begins the analysis of a single binary file using the specified analyzer. - Uses the :analyzeBinary endpoint required by GA API 2025-11-01. - - Args: - analyzer_id (str): The ID of the analyzer to use. - file_bytes (bytes): The content of the file to analyze as bytes. - - Returns: - Response: The response from the analysis request. - - Raises: - HTTPError: If the HTTP request returned an unsuccessful status code. - """ - headers = {"Content-Type": "application/octet-stream"} - headers.update(self._headers) - - response = requests.post( - url=self._get_analyze_binary_url( - self._endpoint, self._api_version, analyzer_id - ), - headers=headers, - data=file_bytes, - ) - - self._raise_for_status_with_detail(response) - self._logger.info( - f"Analyzing binary file with analyzer: {analyzer_id}" - ) - return response - - def begin_analyze_binary(self, analyzer_id: str, file_location: str) -> Response: - """ - Begins the analysis of a single binary file using the specified analyzer. - Uses the :analyzeBinary endpoint required by GA API 2025-11-01. - - Args: - analyzer_id (str): The ID of the analyzer to use. - file_location (str): The local path to the file to analyze. - - Returns: - Response: The response from the analysis request. - - Raises: - ValueError: If the file location is not a valid file path. - HTTPError: If the HTTP request returned an unsuccessful status code. - """ - file_path = Path(file_location) - if not file_path.exists() or not file_path.is_file(): - raise ValueError("File location must be a valid file path.") - - with open(file_location, "rb") as file: - file_bytes = file.read() - - headers = {"Content-Type": "application/octet-stream"} - headers.update(self._headers) - - response = requests.post( - url=self._get_analyze_binary_url( - self._endpoint, self._api_version, analyzer_id - ), - headers=headers, - data=file_bytes, - ) - - self._raise_for_status_with_detail(response) - self._logger.info( - f"Analyzing binary file {file_location} with analyzer: {analyzer_id}" - ) - return response - - def get_prebuilt_document_analyze_result(self, file_location: str) -> Dict[str, Any]: - # Use begin_analyze_binary for single file analysis - response = self.begin_analyze_binary( - analyzer_id=self.PREBUILT_DOCUMENT_ANALYZER_ID, - file_location=file_location, - ) - - return self.poll_result(response, timeout_seconds=POLL_TIMEOUT_SECONDS) - - async def _upload_file_to_blob( - self, container_client: ContainerClient, file_path: str, target_blob_path: str - ) -> None: - with open(file_path, "rb") as data: - await container_client.upload_blob(name=target_blob_path, data=data, overwrite=True) - self._logger.info(f"Uploaded file to {target_blob_path}") - - async def _upload_json_to_blob( - self, container_client: ContainerClient, data: Dict[str, Any], target_blob_path: str - ) -> None: - json_str = json.dumps(data, indent=4) - json_bytes = json_str.encode('utf-8') - await container_client.upload_blob(name=target_blob_path, data=json_bytes, overwrite=True) - self._logger.info(f"Uploaded json to {target_blob_path}") - - async def upload_jsonl_to_blob( - self, container_client: ContainerClient, data_list: List[Dict[str, Any]], target_blob_path: str - ) -> None: - jsonl_string = "\n".join(json.dumps(record) for record in data_list) - jsonl_bytes = jsonl_string.encode("utf-8") - await container_client.upload_blob(name=target_blob_path, data=jsonl_bytes, overwrite=True) - self._logger.info(f"Uploaded jsonl to blob '{target_blob_path}'") - - async def generate_training_data_on_blob( - self, - training_docs_folder: str, - storage_container_sas_url: str, - storage_container_path_prefix: str, - ) -> None: - if not storage_container_path_prefix.endswith("/"): - storage_container_path_prefix += "/" - - async with ContainerClient.from_container_url(storage_container_sas_url) as container_client: - for filename in os.listdir(training_docs_folder): - file_path = os.path.join(training_docs_folder, filename) - _, file_ext = os.path.splitext(filename) - if os.path.isfile(file_path) and ( - file_ext == "" or file_ext.lower() in self.SUPPORTED_FILE_TYPES_DOCUMENT): - # Training feature only supports Standard mode with document data - # Document files uploaded to AI Foundry will be convert to uuid without extension - label_filename = filename + self.LABEL_FILE_SUFFIX - label_path = os.path.join(training_docs_folder, label_filename) - ocr_result_filename = filename + self.OCR_RESULT_FILE_SUFFIX - ocr_result_path = os.path.join(training_docs_folder, ocr_result_filename) - if os.path.exists(label_path) and os.path.exists(ocr_result_path): - file_blob_path = storage_container_path_prefix + filename - label_blob_path = storage_container_path_prefix + label_filename - ocr_result_blob_path = storage_container_path_prefix + ocr_result_filename - - # Upload files - await self._upload_file_to_blob(container_client, file_path, file_blob_path) - await self._upload_file_to_blob(container_client, label_path, label_blob_path) - await self._upload_file_to_blob(container_client, ocr_result_path, ocr_result_blob_path) - self._logger.info(f"Uploaded training data for {filename}") - else: - raise FileNotFoundError( - f"Label file '{label_filename}' or OCR result file '{ocr_result_filename}' " - f"does not exist in '{training_docs_folder}'. " - f"Please ensure both files exist for '{filename}'." - ) - - def _get_analyze_list( - self, - reference_docs_folder: str, - ) -> List[ReferenceDocItem]: - """ - Returns a list of ReferenceDocItem objects for files in the given folder that need to be analyzed. - """ - analyze_list: List[ReferenceDocItem] = [] - - for dirpath, _, filenames in os.walk(reference_docs_folder): - for filename in filenames: - _, file_ext = os.path.splitext(filename) - if self.is_supported_doc_type_by_file_ext(file_ext, is_document=True): - file_path = os.path.join(dirpath, filename) - result_file_name = filename + self.OCR_RESULT_FILE_SUFFIX - analyze_list.append( - ReferenceDocItem( - filename=filename, - file_path=file_path, - result_file_name=result_file_name, - ) - ) - else: - raise ValueError( - f"File '{filename}' is not a supported document type, " - f"please remove it or convert it to a supported type." - ) - - return analyze_list - - def _get_upload_only_list( - self, - reference_docs_folder: str, - ) -> List[ReferenceDocItem]: - """ - Returns a list of ReferenceDocItem objects for files in the given folder that already have OCR results - """ - upload_only_list: List[ReferenceDocItem] = [] - - for dirpath, _, filenames in os.walk(reference_docs_folder): - for filename in filenames: - _, file_ext = os.path.splitext(filename) - if self.is_supported_doc_type_by_file_ext(file_ext, is_document=True): - file_path = os.path.join(dirpath, filename) - result_file_name = filename + self.OCR_RESULT_FILE_SUFFIX - result_file_path = os.path.join(dirpath, result_file_name) - if not os.path.exists(result_file_path): - raise FileNotFoundError( - f"Result file '{result_file_name}' does not exist in '{dirpath}'. " - f"Please run analyze first or remove this file from the folder." - ) - upload_only_list.append( - ReferenceDocItem( - filename=filename, - file_path=file_path, - result_file_name=result_file_name, - result_file_path=result_file_path, - ) - ) - elif filename.endswith(self.OCR_RESULT_FILE_SUFFIX): - original_filename = filename.replace(self.OCR_RESULT_FILE_SUFFIX, "") - if original_filename in filenames: - # skip result.json files corresponding to the file with supported document type - _, original_file_ext = os.path.splitext(original_filename) - if self.is_supported_doc_type_by_file_ext(original_file_ext, is_document=True): - continue - else: - raise ValueError( - f"The '{original_filename}' is not a supported document type, " - f"please remove the result file '{filename}' and '{original_filename}'." - ) - else: - raise ValueError( - f"Result file '{filename}' is not corresponding to an original file, " - f"please remove it." - ) - else: - raise ValueError( - f"File '{filename}' is not a supported document type, " - f"please remove it or convert it to a supported type." - ) - - return upload_only_list - - async def generate_knowledge_base_on_blob( - self, - reference_docs_folder: str, - storage_container_sas_url: str, - storage_container_path_prefix: str, - skip_analyze: bool = False, - ) -> None: - """ - Generates a knowledge base on Azure Blob Storage by analyzing or uploading files from the given folder. - Args: - reference_docs_folder (str): The local path to the folder containing reference documents. - storage_container_sas_url (str): The SAS URL of the Azure Blob Storage container. - storage_container_path_prefix (str): The path prefix within the storage container where files will be - skip_analyze (bool): If True, skips the analysis step and only uploads existing result files. - """ - if not storage_container_path_prefix.endswith("/"): - storage_container_path_prefix += "/" - - resources = [] - async with ContainerClient.from_container_url(storage_container_sas_url) as container_client: - if not skip_analyze: - analyze_list = self._get_analyze_list(reference_docs_folder) - for analyze_item in analyze_list: - self._logger.info(f"Analyzing result for {analyze_item.filename}") - try: - analyze_result = self.get_prebuilt_document_analyze_result(analyze_item.file_path) - except Exception as e: - self._logger.error( - f"Error of getting analyze result of '{analyze_item.filename}'. " - f"Please check the error message and consider retrying or removing this file." - ) - raise e - result_file_blob_path = storage_container_path_prefix + analyze_item.result_file_name - file_blob_path = storage_container_path_prefix + analyze_item.filename - await self._upload_json_to_blob(container_client, analyze_result, result_file_blob_path) - await self._upload_file_to_blob(container_client, analyze_item.file_path, file_blob_path) - resources.append({"file": analyze_item.filename, "resultFile": analyze_item.result_file_name}) - else: - upload_list = self._get_upload_only_list(reference_docs_folder) - for upload_item in upload_list: - self._logger.info(f"Using existing result.json for '{upload_item.filename}'") - result_file_blob_path = storage_container_path_prefix + upload_item.result_file_name - file_blob_path = storage_container_path_prefix + upload_item.filename - await self._upload_file_to_blob(container_client, upload_item.file_path, file_blob_path) - await self._upload_file_to_blob(container_client, upload_item.result_file_path, result_file_blob_path) - resources.append({"file": upload_item.filename, "resultFile": upload_item.result_file_name}) - - # Upload sources.jsonl - await self.upload_jsonl_to_blob( - container_client, resources, storage_container_path_prefix + self.KNOWLEDGE_SOURCE_LIST_FILE_NAME) - - def get_result_file( - self, analyze_response: Response, file_id: str - ) -> Optional[bytes]: - """Retrieves a result file from the analyze operation using the file ID. - - This method can be used to retrieve various types of result files including: - - Key frame images (e.g., 'keyframes/1000') - - Face images (e.g., 'faces/{faceId}') - - Args: - analyze_response (Response): The response object from the analyze operation. - file_id (str): The ID/path of the file to retrieve (e.g., 'keyframes/1000', 'faces/{faceId}'). - Returns: - bytes: The file content as a byte string, or None if retrieval fails. - """ - operation_location = analyze_response.headers.get("operation-location", "") - if not operation_location: - raise ValueError( - "Operation location not found in the analyzer response header." - ) - # Extract operation ID from operation-location - # Format: {endpoint}/contentunderstanding/analyzerResults/{operationId}?api-version={version} - operation_location_without_params = operation_location.split("?api-version")[0] - operation_id = operation_location_without_params.split("/")[-1] - - # Construct file retrieval URL according to TypeSpec: /analyzerResults/{operationId}/files/{+path} - file_retrieval_url = ( - f"{self._endpoint}/contentunderstanding/analyzerResults/{operation_id}/files/{file_id}?api-version={self._api_version}" - ) - try: - response = requests.get(url=file_retrieval_url, headers=self._headers) - self._raise_for_status_with_detail(response) - return response.content - except requests.exceptions.RequestException as e: - print(f"HTTP request failed: {e}") - return None - - def begin_create_classifier( - self, - classifier_id: str, - classifier_schema: Dict[str, Any], - ) -> Response: - """ - Initiates the creation of an classifier with the given ID and schema. - - Args: - classifier_id (str): The unique identifier for the classifier. - classifier_schema (dict): The schema definition for the classifier. - - Raises: - requests.exceptions.HTTPError: If the HTTP request to create the classifier fails. - ValueError: If the classifier schema or ID is not provided. - - Returns: - requests.Response: The response object from the HTTP request. - """ - - if not classifier_schema: - raise ValueError("Classifier schema must be provided.") - if not classifier_id: - raise ValueError("Classifier ID must be provided.") - - headers = {"Content-Type": "application/json"} - headers.update(self._headers) - - response = requests.put( - url=self._get_classifier_url(self._endpoint, self._api_version, classifier_id), - headers=headers, - json=classifier_schema, - ) - self._raise_for_status_with_detail(response) - self._logger.info(f"Classifier {classifier_id} create request accepted.") - return response - - def begin_classify(self, classifier_id: str, file_location: str) -> Response: - """ - Begins the analysis of a file or URL using the specified classifier. - - Args: - classifier_id (str): The ID of the classifier to use. - file_location (str): The local path to the file or the URL to analyze. - - Returns: - Response: The response from the analysis request. - - Raises: - ValueError: If the file location is not a valid path or URL. - HTTPError: If the HTTP request returned an unsuccessful status code. - """ - data = None - if Path(file_location).exists(): - with open(file_location, "rb") as file: - data = file.read() - headers = {"Content-Type": "application/octet-stream"} - elif "https://" in file_location or "http://" in file_location: - data = {"url": file_location} - headers = {"Content-Type": "application/json"} - else: - raise ValueError("File location must be a valid path or URL.") - - headers.update(self._headers) - if isinstance(data, dict): - response = requests.post( - url=self._get_classify_url( - self._endpoint, self._api_version, classifier_id - ), - headers=headers, - json=data, - ) - else: - response = requests.post( - url=self._get_classify_url( - self._endpoint, self._api_version, classifier_id - ), - headers=headers, - data=data, - ) - - self._raise_for_status_with_detail(response) - self._logger.info( - f"Analyzing file {file_location} with classifier_id: {classifier_id}" - ) - return response - - def poll_result( - self, - response: Response, - timeout_seconds: int = POLL_TIMEOUT_SECONDS, - polling_interval_seconds: int = 2, - ) -> Dict[str, Any]: - """ - Polls the result of an asynchronous operation until it completes or times out. - - Args: - response (Response): The initial response object containing the operation location. - timeout_seconds (int, optional): The maximum number of seconds to wait for the operation to complete. Defaults to 120. - polling_interval_seconds (int, optional): The number of seconds to wait between polling attempts. Defaults to 2. - - Raises: - ValueError: If the operation location is not found in the response headers. - TimeoutError: If the operation does not complete within the specified timeout. - RuntimeError: If the operation fails. - - Returns: - dict: The JSON response of the completed operation if it succeeds. - """ - operation_location = response.headers.get("operation-location", "") - if not operation_location: - raise ValueError("Operation location not found in response headers.") - - headers = {"Content-Type": "application/json"} - headers.update(self._headers) - - start_time = time.time() - while True: - elapsed_time = time.time() - start_time - if elapsed_time > timeout_seconds: - raise TimeoutError( - f"Operation timed out after {timeout_seconds:.2f} seconds." - ) - - response = requests.get(operation_location, headers=self._headers) - self._raise_for_status_with_detail(response) - status = response.json().get("status").lower() - if status == "succeeded": - self._logger.info( - f"Request result is ready after {elapsed_time:.2f} seconds." - ) - return response.json() - elif status == "failed": - self._logger.error(f"Request failed. Reason: {response.json()}") - raise RuntimeError("Request failed.") - else: - self._logger.info( - f"Request {operation_location.split('/')[-1].split('?')[0]} in progress ..." - ) - time.sleep(polling_interval_seconds) \ No newline at end of file diff --git a/app/services/document_intelligence_client.py b/app/services/document_intelligence_client.py deleted file mode 100644 index 2e0e196..0000000 --- a/app/services/document_intelligence_client.py +++ /dev/null @@ -1,446 +0,0 @@ -""" -Azure Document Intelligence Client for document analysis. - -This module provides a client for Azure Document Intelligence service -that extracts markdown content including text, tables, images, and charts. -""" - -import asyncio -import base64 -import os -import time -from typing import Optional, Dict, List, Tuple - -from azure.ai.documentintelligence import DocumentIntelligenceClient -from azure.ai.documentintelligence.models import ( - AnalyzeDocumentRequest, - DocumentContentFormat, - AnalyzeResult, - AnalyzeOutputOption, -) -from azure.core.credentials import AzureKeyCredential -from azure.identity import DefaultAzureCredential -from dotenv import load_dotenv - -from .logging_config import get_logger - -load_dotenv() - -# Get logger from centralized config -logger = get_logger(__name__) - - -class AzureDocumentIntelligenceClient: - """ - Client for Azure Document Intelligence service. - - Extracts content from documents as markdown, including: - - Text content - - Tables - - Images/Figures with descriptions - - Charts - """ - - def __init__(self): - """Initialize the Document Intelligence client.""" - logger.info("Initializing AzureDocumentIntelligenceClient...") - init_start = time.time() - - # Get endpoint from environment - use dedicated DI endpoint or fallback to AI endpoint - self.endpoint = os.getenv("AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT") or os.getenv("AZURE_CONTENT_UNDERSTANDING_ENDPOINT") - - if not self.endpoint: - raise ValueError( - "AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT or AZURE_CONTENT_UNDERSTANDING_ENDPOINT environment variable is required. " - "Please set it in your .env file." - ) - - logger.info("Using Document Intelligence endpoint: %s...", self.endpoint[:50]) - - # Check if API key is provided, otherwise use token auth - api_key = os.getenv("AZURE_DOCUMENT_INTELLIGENCE_KEY") or os.getenv("AZURE_AI_API_KEY") - - if api_key: - self.client = DocumentIntelligenceClient( - endpoint=self.endpoint, - credential=AzureKeyCredential(api_key) - ) - auth_method = "API Key" - else: - self.client = DocumentIntelligenceClient( - endpoint=self.endpoint, - credential=DefaultAzureCredential() - ) - auth_method = "Azure AD Token" - - logger.info("Authentication method: %s", auth_method) - - init_duration = time.time() - init_start - logger.info("AzureDocumentIntelligenceClient initialized in %.2fs", init_duration) - - # Maximum file size in bytes (500 MB for standard tier) - MAX_FILE_SIZE_BYTES = 500 * 1024 * 1024 # 500 MB - - def analyze_document( - self, - file_bytes: bytes, - request_id: str = "unknown", - include_figures: bool = True - ) -> str: - """ - Analyze a document and return content as markdown. - - Args: - file_bytes: The document content as bytes - request_id: Unique request ID for logging correlation - include_figures: Whether to include figure/image analysis - - Returns: - Extracted content as markdown string - - Raises: - ValueError: If file size exceeds maximum allowed size - """ - analyze_start = time.time() - - # Check file size before sending - file_size_mb = len(file_bytes) / (1024 * 1024) - logger.info("[REQ:%s] Starting Document Intelligence analysis (file size: %.2f MB)...", - request_id, file_size_mb) - - if len(file_bytes) > self.MAX_FILE_SIZE_BYTES: - raise ValueError( - f"Document size ({file_size_mb:.2f} MB) exceeds maximum allowed size " - f"({self.MAX_FILE_SIZE_BYTES / (1024 * 1024):.0f} MB). " - "Please use a smaller document or split it into multiple parts." - ) - - try: - # Use prebuilt-layout model for comprehensive document analysis - # Include FIGURES output option to enable figure extraction with cropped images - output_options = [AnalyzeOutputOption.FIGURES] if include_figures else None - - poller = self.client.begin_analyze_document( - model_id="prebuilt-layout", - body=file_bytes, - content_type="application/octet-stream", - output_content_format=DocumentContentFormat.MARKDOWN, - output=output_options, - ) - - logger.info("[REQ:%s] Document analysis started, waiting for result...", request_id) - - result: AnalyzeResult = poller.result() - operation_id = poller.details.get("operation_id") - - analyze_duration = time.time() - analyze_start - logger.info("[REQ:%s] Document analysis completed in %.2fs", request_id, analyze_duration) - - # Extract markdown content including figure descriptions - markdown_content = self._build_markdown_from_result( - result, - request_id, - operation_id, - include_figures - ) - - logger.info("[REQ:%s] Extracted %d characters of markdown content", - request_id, len(markdown_content)) - - return markdown_content - - except Exception as e: - logger.error("[REQ:%s] Document analysis failed: %s", request_id, str(e)) - raise - - def _build_markdown_from_result( - self, - result: AnalyzeResult, - request_id: str, - operation_id: str = None, - include_figures: bool = True - ) -> str: - """ - Build markdown content from analysis result. - - Includes the main content plus descriptions of figures, tables, and charts. - - Args: - result: The analyze result from Document Intelligence - request_id: Request ID for logging - operation_id: The operation ID for retrieving figure images - include_figures: Whether to include figure descriptions - - Returns: - Complete markdown content - """ - # Start with the main content - markdown_parts = [] - - # The content property contains the extracted text in markdown format - if result.content: - markdown_parts.append(result.content) - - # Add figure/image descriptions if available - if include_figures and hasattr(result, 'figures') and result.figures: - figure_section = self._extract_figure_descriptions( - result.figures, - request_id, - result.model_id, - operation_id - ) - if figure_section: - markdown_parts.append("\n\n---\n\n## Figures and Images\n\n") - markdown_parts.append(figure_section) - - return "".join(markdown_parts) - - def _extract_figure_descriptions( - self, - figures: list, - request_id: str, - model_id: str = None, - operation_id: str = None - ) -> str: - """ - Extract descriptions from figures/images. - - Args: - figures: List of figure objects from the analysis - request_id: Request ID for logging - model_id: The model ID used for analysis - operation_id: The operation ID for retrieving figure images - - Returns: - Markdown formatted figure descriptions - """ - if not figures: - return "" - - descriptions = [] - for i, figure in enumerate(figures, 1): - figure_md_parts = [f"### Figure {i}"] - - # Get figure ID for potential image retrieval - figure_id = getattr(figure, 'id', None) - if figure_id: - figure_md_parts.append(f"\n**Figure ID:** {figure_id}") - - # Get caption if available - caption = "" - if hasattr(figure, 'caption') and figure.caption: - if hasattr(figure.caption, 'content'): - caption = figure.caption.content - else: - caption = str(figure.caption) - if caption: - figure_md_parts.append(f"\n**Caption:** {caption}") - - # Get bounding regions (location info) - if hasattr(figure, 'bounding_regions') and figure.bounding_regions: - for region in figure.bounding_regions: - page_num = getattr(region, 'page_number', 'unknown') - figure_md_parts.append(f"\n**Location:** Page {page_num}") - - # Get spans information (text content related to the figure) - if hasattr(figure, 'spans') and figure.spans: - span_info = [] - for span in figure.spans: - offset = getattr(span, 'offset', None) - length = getattr(span, 'length', None) - if offset is not None and length is not None: - span_info.append(f"offset={offset}, length={length}") - if span_info: - figure_md_parts.append(f"\n**Text Spans:** {'; '.join(span_info)}") - - # Get any additional elements in the figure - elements_content = [] - if hasattr(figure, 'elements') and figure.elements: - for elem in figure.elements[:10]: # Limit to avoid too much content - if isinstance(elem, str): - elements_content.append(elem) - elif hasattr(elem, 'content'): - elements_content.append(elem.content) - if elements_content: - figure_md_parts.append(f"\n**Related Content:** {'; '.join(elements_content)}") - - # Get footnotes if available - if hasattr(figure, 'footnotes') and figure.footnotes: - footnotes = [] - for fn in figure.footnotes: - if hasattr(fn, 'content'): - footnotes.append(fn.content) - else: - footnotes.append(str(fn)) - if footnotes: - figure_md_parts.append(f"\n**Footnotes:** {'; '.join(footnotes)}") - - descriptions.append("".join(figure_md_parts)) - - logger.info("[REQ:%s] Extracted %d figure descriptions", request_id, len(descriptions)) - return "\n\n".join(descriptions) - - def get_figure_image( - self, - model_id: str, - operation_id: str, - figure_id: str, - request_id: str = "unknown" - ) -> bytes: - """ - Retrieve the cropped image for a specific figure. - - Args: - model_id: The model ID used for analysis - operation_id: The operation/result ID from the analysis - figure_id: The figure ID to retrieve - request_id: Request ID for logging - - Returns: - Image bytes (PNG format) - """ - try: - logger.info("[REQ:%s] Retrieving figure image: %s", request_id, figure_id) - response = self.client.get_analyze_result_figure( - model_id=model_id, - result_id=operation_id, - figure_id=figure_id - ) - # Read all bytes from the response - image_bytes = b"".join(response) - logger.info("[REQ:%s] Retrieved figure image, size: %d bytes", request_id, len(image_bytes)) - return image_bytes - except Exception as e: - logger.error("[REQ:%s] Failed to retrieve figure image %s: %s", request_id, figure_id, str(e)) - raise - - def analyze_document_with_figures( - self, - file_bytes: bytes, - request_id: str = "unknown" - ) -> Tuple[str, Dict[str, bytes]]: - """ - Analyze a document and return markdown content along with figure images. - - This method extracts both the markdown content and the actual figure images - as cropped PNG files. - - Args: - file_bytes: The document content as bytes - request_id: Unique request ID for logging correlation - - Returns: - Tuple of (markdown_content, dict of figure_id -> image_bytes) - """ - analyze_start = time.time() - - # Check file size before sending - file_size_mb = len(file_bytes) / (1024 * 1024) - logger.info("[REQ:%s] Starting Document Intelligence analysis with figures (file size: %.2f MB)...", - request_id, file_size_mb) - - if len(file_bytes) > self.MAX_FILE_SIZE_BYTES: - raise ValueError( - f"Document size ({file_size_mb:.2f} MB) exceeds maximum allowed size " - f"({self.MAX_FILE_SIZE_BYTES / (1024 * 1024):.0f} MB). " - "Please use a smaller document or split it into multiple parts." - ) - - try: - # Use prebuilt-layout model with FIGURES output option - poller = self.client.begin_analyze_document( - model_id="prebuilt-layout", - body=file_bytes, - content_type="application/octet-stream", - output_content_format=DocumentContentFormat.MARKDOWN, - output=[AnalyzeOutputOption.FIGURES], - ) - - logger.info("[REQ:%s] Document analysis started, waiting for result...", request_id) - - result: AnalyzeResult = poller.result() - operation_id = poller.details.get("operation_id") - - analyze_duration = time.time() - analyze_start - logger.info("[REQ:%s] Document analysis completed in %.2fs", request_id, analyze_duration) - - # Extract markdown content - markdown_content = self._build_markdown_from_result( - result, - request_id, - operation_id, - include_figures=True - ) - - # Extract figure images - figure_images = {} - if result.figures and operation_id: - for figure in result.figures: - figure_id = getattr(figure, 'id', None) - if figure_id: - try: - image_bytes = self.get_figure_image( - model_id=result.model_id, - operation_id=operation_id, - figure_id=figure_id, - request_id=request_id - ) - figure_images[figure_id] = image_bytes - except Exception as e: - logger.warning("[REQ:%s] Could not retrieve figure %s: %s", - request_id, figure_id, str(e)) - - logger.info("[REQ:%s] Extracted %d characters of markdown and %d figure images", - request_id, len(markdown_content), len(figure_images)) - - return markdown_content, figure_images - - except Exception as e: - logger.error("[REQ:%s] Document analysis with figures failed: %s", request_id, str(e)) - raise - - async def analyze_document_with_figures_async( - self, - file_bytes: bytes, - request_id: str = "unknown" - ) -> Tuple[str, Dict[str, bytes]]: - """ - Async version of analyze_document_with_figures. - - Args: - file_bytes: The document content as bytes - request_id: Unique request ID for logging correlation - - Returns: - Tuple of (markdown_content, dict of figure_id -> image_bytes) - """ - return await asyncio.to_thread( - self.analyze_document_with_figures, - file_bytes, - request_id - ) - - async def analyze_document_async( - self, - file_bytes: bytes, - request_id: str = "unknown", - include_figures: bool = True - ) -> str: - """ - Async version of analyze_document for parallel processing. - - Args: - file_bytes: The document content as bytes - request_id: Unique request ID for logging correlation - include_figures: Whether to include figure/image analysis - - Returns: - Extracted content as markdown string - """ - # Run the blocking API call in a thread pool - return await asyncio.to_thread( - self.analyze_document, - file_bytes, - request_id, - include_figures - ) diff --git a/app/services/document_processor.py b/app/services/document_processor.py deleted file mode 100644 index f9bd2f4..0000000 --- a/app/services/document_processor.py +++ /dev/null @@ -1,209 +0,0 @@ -""" -Document Processing Service using Azure AI Services. - -This module handles document processing and extraction using either: -- Azure Content Understanding (default) -- Azure Document Intelligence - -Both services convert various document formats to markdown. -""" - -import os -import time -import uuid -from datetime import datetime -from enum import Enum -from typing import Optional -from azure.identity import DefaultAzureCredential -from dotenv import load_dotenv - -from .content_understanding_client import AzureContentUnderstandingClient -from .document_intelligence_client import AzureDocumentIntelligenceClient -from .logging_config import get_logger - -load_dotenv() - -# Get logger from centralized config -logger = get_logger(__name__) - - -class ExtractionService(str, Enum): - """Available document extraction services.""" - CONTENT_UNDERSTANDING = "content_understanding" - DOCUMENT_INTELLIGENCE = "document_intelligence" - - -class DocumentProcessor: - """ - Document processor using Azure AI Services. - - Supports multiple extraction backends: - - Azure Content Understanding - - Azure Document Intelligence - - Processes various document formats and extracts content as markdown. - """ - - def __init__(self, service: ExtractionService = ExtractionService.CONTENT_UNDERSTANDING): - """ - Initialize the document processor with specified service. - - Args: - service: The extraction service to use (default: Content Understanding) - """ - logger.info("Initializing DocumentProcessor with service: %s...", service.value) - init_start = time.time() - - self.service = service - self.cu_client: Optional[AzureContentUnderstandingClient] = None - self.di_client: Optional[AzureDocumentIntelligenceClient] = None - - if service == ExtractionService.CONTENT_UNDERSTANDING: - self._init_content_understanding() - else: - self._init_document_intelligence() - - init_duration = time.time() - init_start - logger.info("DocumentProcessor initialized in %.2fs", init_duration) - - def _init_content_understanding(self): - """Initialize Azure Content Understanding client.""" - self.endpoint = os.getenv("AZURE_CONTENT_UNDERSTANDING_ENDPOINT") - - if not self.endpoint: - raise ValueError( - "AZURE_CONTENT_UNDERSTANDING_ENDPOINT environment variable is required. " - "Please set it in your .env file." - ) - - logger.info("Using Azure AI endpoint: %s...", self.endpoint[:50]) - - # Create token provider using DefaultAzureCredential - def token_provider(): - credential = DefaultAzureCredential() - token = credential.get_token("https://cognitiveservices.azure.com/.default") - return token.token - - # Check if API key is provided, otherwise use token auth - api_key = os.getenv("AZURE_AI_API_KEY") - auth_method = "API Key" if api_key else "Azure AD Token" - logger.info("Authentication method: %s", auth_method) - - self.cu_client = AzureContentUnderstandingClient( - endpoint=self.endpoint, - api_version="2025-11-01", - subscription_key=api_key, - token_provider=token_provider if not api_key else None, - ) - - def _init_document_intelligence(self): - """Initialize Azure Document Intelligence client.""" - self.di_client = AzureDocumentIntelligenceClient() - - async def extract_content(self, file_bytes: bytes, filename: str) -> str: - """ - Extract content from a document and return as markdown. - - Uses the configured extraction service (Content Understanding or Document Intelligence). - - Args: - file_bytes: The document content as bytes - filename: The original filename (used to determine file type) - - Returns: - Extracted content as markdown string - """ - import asyncio - - # Generate unique request ID for tracking this extraction - request_id = str(uuid.uuid4())[:8] - extract_start = time.time() - logger.info("[REQ:%s] Starting content extraction for: %s (%d bytes) using %s", - request_id, filename, len(file_bytes), self.service.value) - - # Determine content type based on filename extension - extension = filename.lower().split(".")[-1] if "." in filename else "" - logger.info("[REQ:%s] Detected file extension: %s", request_id, extension) - - # Handle plain text and markdown files directly - if extension in ["txt", "md"]: - logger.info("[REQ:%s] Processing as plain text/markdown (no Azure API call needed)", request_id) - content = file_bytes.decode("utf-8") - duration = time.time() - extract_start - logger.info("[REQ:%s] Text extraction completed in %.3fs (%d chars)", - request_id, duration, len(content)) - return content - - # Use the configured extraction service - if self.service == ExtractionService.CONTENT_UNDERSTANDING: - logger.info("[REQ:%s] Processing with Azure Content Understanding (format: %s)...", - request_id, extension) - content = await asyncio.to_thread(self._analyze_with_content_understanding, file_bytes, request_id) - else: - logger.info("[REQ:%s] Processing with Azure Document Intelligence (format: %s)...", - request_id, extension) - content = await self.di_client.analyze_document_async(file_bytes, request_id) - - duration = time.time() - extract_start - logger.info("[REQ:%s] โœ… Document extraction completed in %.3fs (%d chars)", - request_id, duration, len(content)) - return content - - def _analyze_with_content_understanding(self, file_bytes: bytes, request_id: str = "unknown") -> str: - """ - Analyze document using Azure Content Understanding. - - Args: - file_bytes: The document content as bytes - request_id: Unique request ID for logging correlation - - Returns: - Extracted content as markdown string - """ - analyze_start = time.time() - logger.info("[REQ:%s] ๐Ÿ“ค Calling Azure Content Understanding API...", request_id) - - # Use the prebuilt-documentSearch analyzer for document analysis - # This extracts text, tables, and structure from documents as markdown - api_call_start = time.time() - response = self.cu_client.begin_analyze_binary_bytes(analyzer_id="prebuilt-documentSearch", file_bytes=file_bytes) - api_call_duration = time.time() - api_call_start - logger.info("[REQ:%s] API call initiated in %.3fs", request_id, api_call_duration) - - logger.info("[REQ:%s] โณ Polling for analysis result...", request_id) - poll_start = time.time() - result = self.cu_client.poll_result(response) - poll_duration = time.time() - poll_start - logger.info("[REQ:%s] โœ… Polling completed in %.3fs", request_id, poll_duration) - - # Extract markdown from the first content element - parse_start = time.time() - contents = result.get("result", {}).get("contents", []) - if contents: - content = contents[0] - markdown = content.get("markdown", "") - parse_duration = time.time() - parse_start - analyze_duration = time.time() - analyze_start - logger.info("[REQ:%s] Azure Content Understanding analysis completed:", request_id) - logger.info("[REQ:%s] - API initiation: %.3fs", request_id, api_call_duration) - logger.info("[REQ:%s] - Polling wait: %.3fs", request_id, poll_duration) - logger.info("[REQ:%s] - Parse result: %.3fs", request_id, parse_duration) - logger.info("[REQ:%s] - Total: %.3fs", request_id, analyze_duration) - return markdown - - logger.warning("[REQ:%s] โš ๏ธ No content extracted from document", request_id) - return "" - - def extract_content_sync(self, file_bytes: bytes, filename: str) -> str: - """ - Synchronous version of extract_content for non-async contexts. - - Args: - file_bytes: The document content as bytes - filename: The original filename (used to determine file type) - - Returns: - Extracted content as markdown string - """ - import asyncio - return asyncio.run(self.extract_content(file_bytes, filename)) diff --git a/app/services/logging_config.py b/app/services/logging_config.py deleted file mode 100644 index 2778893..0000000 --- a/app/services/logging_config.py +++ /dev/null @@ -1,238 +0,0 @@ -""" -Centralized Logging Configuration for RFP Analyzer. - -This module provides unified logging configuration with: -- Console handler (stdout) -- File handler (rotating log files) -- OpenTelemetry handler (Azure Monitor / Log Analytics) - -Usage: - from services.logging_config import setup_logging, get_logger - - # Call once at application startup (in main.py) - setup_logging() - - # Get logger in any module - logger = get_logger(__name__) -""" - -import os -import sys -import logging -from logging.handlers import RotatingFileHandler -from pathlib import Path -from typing import Optional - -# Default configuration -DEFAULT_LOG_FORMAT = '%(asctime)s.%(msecs)03d | %(levelname)s | %(name)s | %(message)s' -DEFAULT_DATE_FORMAT = '%Y-%m-%d %H:%M:%S' -DEFAULT_LOG_LEVEL = logging.INFO -LOG_DIR = Path(__file__).parent.parent / "logs" -LOG_FILE = "rfp_analyzer.log" -MAX_LOG_SIZE = 10 * 1024 * 1024 # 10 MB -BACKUP_COUNT = 5 - -# Track if logging has been configured -_logging_configured = False - - -def _get_otel_enabled_default() -> bool: - """Get default value for log_to_otel from environment variable.""" - env_value = os.getenv("OTEL_LOGGING_ENABLED", "false").lower() - return env_value in ("true", "1", "yes", "on") - - -def setup_logging( - level: int = DEFAULT_LOG_LEVEL, - log_to_console: bool = True, - log_to_file: bool = True, - log_to_otel: Optional[bool] = None, # Defaults to OTEL_LOGGING_ENABLED env var - log_dir: Optional[Path] = None, - connection_string: Optional[str] = None, -) -> None: - """ - Configure unified logging for the application. - - Args: - level: Logging level (default: INFO) - log_to_console: Enable console logging (default: True) - log_to_file: Enable file logging (default: True) - log_to_otel: Enable OpenTelemetry/Azure Monitor logging - (default: from OTEL_LOGGING_ENABLED env var, or True if not set) - log_dir: Directory for log files (default: app/logs) - connection_string: App Insights connection string (default: from env) - """ - global _logging_configured - - if _logging_configured: - return - - # Resolve log_to_otel default from environment if not explicitly provided - if log_to_otel is None: - log_to_otel = _get_otel_enabled_default() - - # Get root logger - root_logger = logging.getLogger() - root_logger.setLevel(level) - - # Clear existing handlers - root_logger.handlers.clear() - - # Create formatter - formatter = logging.Formatter(DEFAULT_LOG_FORMAT, DEFAULT_DATE_FORMAT) - - # Console Handler - if log_to_console: - console_handler = logging.StreamHandler(sys.stdout) - console_handler.setLevel(level) - console_handler.setFormatter(formatter) - root_logger.addHandler(console_handler) - - # File Handler - if log_to_file: - log_path = log_dir or LOG_DIR - log_path.mkdir(parents=True, exist_ok=True) - - file_handler = RotatingFileHandler( - log_path / LOG_FILE, - maxBytes=MAX_LOG_SIZE, - backupCount=BACKUP_COUNT, - encoding='utf-8' - ) - file_handler.setLevel(level) - file_handler.setFormatter(formatter) - root_logger.addHandler(file_handler) - - # OpenTelemetry Handler (Azure Monitor) - # Only initialize if explicitly enabled - no SDK loaded otherwise - otel_enabled = False - if log_to_otel: - conn_str = connection_string or os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING") - if conn_str: - otel_enabled = _setup_otel_logging(root_logger, conn_str, level) - - # Suppress noisy loggers from being sent to all handlers - # These generate high volume logs that can cause throttling (HTTP 439) - _suppress_noisy_loggers() - - _logging_configured = True - - # Log startup message - startup_logger = logging.getLogger("rfp_analyzer.logging") - startup_logger.info("Logging initialized - Console: %s, File: %s, OpenTelemetry: %s", - log_to_console, log_to_file, otel_enabled) - - -def _setup_otel_logging(root_logger: logging.Logger, connection_string: str, level: int) -> bool: - """ - Set up OpenTelemetry logging handler for Azure Monitor. - - Args: - root_logger: The root logger to attach the handler to - connection_string: Azure Monitor connection string - level: Logging level - - Returns: - True if OTEL logging was successfully set up, False otherwise - """ - try: - # Lazy import - only load OTEL SDK when actually needed - from opentelemetry._logs import set_logger_provider - from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler - from opentelemetry.sdk._logs.export import BatchLogRecordProcessor - from azure.monitor.opentelemetry.exporter import AzureMonitorLogExporter - - # Create logger provider - logger_provider = LoggerProvider() - set_logger_provider(logger_provider) - - # Create Azure Monitor exporter with optimized settings - exporter = AzureMonitorLogExporter( - connection_string=connection_string, - # Reduce timeout to avoid long waits (default is 10s) - connection_timeout=5, - ) - - # Add batch processor with optimized settings for better performance - # - Larger batch = fewer HTTP requests - # - Longer interval = less frequent exports - # - Shorter timeout = don't block shutdown too long - logger_provider.add_log_record_processor( - BatchLogRecordProcessor( - exporter, - max_queue_size=2048, # Buffer more logs before dropping (default: 2048) - schedule_delay_millis=5000, # Export every 5 seconds (default: 5000) - max_export_batch_size=512, # Larger batches = fewer HTTP calls (default: 512) - export_timeout_millis=10000, # 10s timeout for export (default: 30000) - ) - ) - - # Create and add OpenTelemetry handler - otel_handler = LoggingHandler( - level=level, - logger_provider=logger_provider - ) - root_logger.addHandler(otel_handler) - return True - - except ImportError: - # OTEL packages not installed - logging.getLogger("rfp_analyzer.logging").warning( - "OpenTelemetry packages not installed. OTEL logging disabled." - ) - return False - except Exception as e: - # Don't fail startup if OTEL setup fails - logging.getLogger("rfp_analyzer.logging").warning( - "Failed to setup OpenTelemetry logging: %s", str(e) - ) - return False - - -def get_logger(name: str) -> logging.Logger: - """ - Get a logger with the specified name. - - This is the preferred way to get a logger in all modules. - - Args: - name: Logger name (typically __name__) - - Returns: - Configured logger instance - """ - return logging.getLogger(name) - - -def set_log_level(level: int, logger_name: Optional[str] = None) -> None: - """ - Dynamically change log level. - - Args: - level: New logging level - logger_name: Specific logger name, or None for root logger - """ - logger = logging.getLogger(logger_name) - logger.setLevel(level) - for handler in logger.handlers: - handler.setLevel(level) - - -def _suppress_noisy_loggers() -> None: - """ - Suppress verbose loggers that generate high volume telemetry. - - These loggers can cause App Insights throttling (HTTP 439) due to - excessive telemetry volume. Set them to WARNING or higher. - """ - noisy_loggers = [ - # Azure SDK HTTP logging - very verbose, logs every HTTP request/response - "azure.core.pipeline.policies.http_logging_policy", - # HTTP libraries - can be very chatty - "urllib3.connectionpool", - "httpx", - "httpcore", - ] - - for logger_name in noisy_loggers: - logging.getLogger(logger_name).setLevel(logging.WARNING) diff --git a/app/services/processing_queue.py b/app/services/processing_queue.py deleted file mode 100644 index 67d40ce..0000000 --- a/app/services/processing_queue.py +++ /dev/null @@ -1,204 +0,0 @@ -""" -Processing Queue for Document Extraction and Proposal Scoring. - -This module provides queue-based processing with progress tracking and timing. -""" - -from dataclasses import dataclass, field -from enum import Enum -from typing import Optional, List, Dict, Any -from datetime import datetime -import time - - -class QueueItemStatus(str, Enum): - """Status of a queue item.""" - PENDING = "pending" - PROCESSING = "processing" - COMPLETED = "completed" - FAILED = "failed" - - -@dataclass -class QueueItem: - """Represents an item in the processing queue.""" - id: str - name: str - item_type: str # "rfp", "proposal", "evaluation" - status: QueueItemStatus = QueueItemStatus.PENDING - start_time: Optional[float] = None - end_time: Optional[float] = None - duration: Optional[float] = None - error_message: Optional[str] = None - result: Optional[Any] = None - metadata: Dict[str, Any] = field(default_factory=dict) - - def start(self): - """Mark the item as processing.""" - self.status = QueueItemStatus.PROCESSING - self.start_time = time.time() - - def complete(self, result: Any = None): - """Mark the item as completed.""" - self.status = QueueItemStatus.COMPLETED - self.end_time = time.time() - if self.start_time: - self.duration = self.end_time - self.start_time - self.result = result - - def fail(self, error_message: str): - """Mark the item as failed.""" - self.status = QueueItemStatus.FAILED - self.end_time = time.time() - if self.start_time: - self.duration = self.end_time - self.start_time - self.error_message = error_message - - def get_elapsed_time(self) -> float: - """Get the elapsed time for this item.""" - if self.duration is not None: - return self.duration - if self.start_time is not None: - return time.time() - self.start_time - return 0.0 - - def get_status_icon(self) -> str: - """Get a status icon for display.""" - icons = { - QueueItemStatus.PENDING: "โณ", - QueueItemStatus.PROCESSING: "๐Ÿ”„", - QueueItemStatus.COMPLETED: "โœ…", - QueueItemStatus.FAILED: "โŒ" - } - return icons.get(self.status, "โ“") - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary for serialization.""" - return { - "id": self.id, - "name": self.name, - "item_type": self.item_type, - "status": self.status.value, - "start_time": self.start_time, - "end_time": self.end_time, - "duration": self.duration, - "error_message": self.error_message, - "metadata": self.metadata - } - - -@dataclass -class ProcessingQueue: - """Manages a queue of items to be processed.""" - name: str - items: List[QueueItem] = field(default_factory=list) - start_time: Optional[float] = None - end_time: Optional[float] = None - - def add_item(self, id: str, name: str, item_type: str, metadata: Optional[Dict[str, Any]] = None) -> QueueItem: - """Add an item to the queue.""" - item = QueueItem( - id=id, - name=name, - item_type=item_type, - metadata=metadata or {} - ) - self.items.append(item) - return item - - def start(self): - """Mark the queue as started.""" - self.start_time = time.time() - - def finish(self): - """Mark the queue as finished.""" - self.end_time = time.time() - - def get_item(self, id: str) -> Optional[QueueItem]: - """Get an item by ID.""" - for item in self.items: - if item.id == id: - return item - return None - - def get_pending_items(self) -> List[QueueItem]: - """Get all pending items.""" - return [item for item in self.items if item.status == QueueItemStatus.PENDING] - - def get_completed_items(self) -> List[QueueItem]: - """Get all completed items.""" - return [item for item in self.items if item.status == QueueItemStatus.COMPLETED] - - def get_failed_items(self) -> List[QueueItem]: - """Get all failed items.""" - return [item for item in self.items if item.status == QueueItemStatus.FAILED] - - def get_progress(self) -> Dict[str, Any]: - """Get overall progress statistics.""" - total = len(self.items) - completed = len([i for i in self.items if i.status == QueueItemStatus.COMPLETED]) - failed = len([i for i in self.items if i.status == QueueItemStatus.FAILED]) - processing = len([i for i in self.items if i.status == QueueItemStatus.PROCESSING]) - pending = len([i for i in self.items if i.status == QueueItemStatus.PENDING]) - - percentage = int((completed + failed) / total * 100) if total > 0 else 0 - - return { - "total": total, - "completed": completed, - "failed": failed, - "processing": processing, - "pending": pending, - "percentage": percentage - } - - def get_total_duration(self) -> float: - """Get total queue processing duration.""" - if self.start_time is None: - return 0.0 - if self.end_time is not None: - return self.end_time - self.start_time - return time.time() - self.start_time - - def get_average_item_duration(self) -> float: - """Get average duration per completed item.""" - completed = [i for i in self.items if i.duration is not None] - if not completed: - return 0.0 - return sum(i.duration for i in completed) / len(completed) - - def is_complete(self) -> bool: - """Check if all items are processed (completed or failed).""" - return all( - item.status in [QueueItemStatus.COMPLETED, QueueItemStatus.FAILED] - for item in self.items - ) - - def clear(self): - """Clear all items from the queue.""" - self.items.clear() - self.start_time = None - self.end_time = None - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary for serialization.""" - return { - "name": self.name, - "items": [item.to_dict() for item in self.items], - "start_time": self.start_time, - "end_time": self.end_time, - "progress": self.get_progress(), - "total_duration": self.get_total_duration() - } - - -def format_duration(seconds: float) -> str: - """Format duration in a human-readable way.""" - if seconds < 1: - return f"{seconds*1000:.0f}ms" - elif seconds < 60: - return f"{seconds:.1f}s" - else: - minutes = int(seconds // 60) - remaining_seconds = seconds % 60 - return f"{minutes}m {remaining_seconds:.1f}s" diff --git a/app/services/scoring_agent.py b/app/services/scoring_agent.py deleted file mode 100644 index b0b59cb..0000000 --- a/app/services/scoring_agent.py +++ /dev/null @@ -1,520 +0,0 @@ -""" -RFP Scoring Agent using Microsoft Agent Framework. - -This module implements an AI agent that evaluates vendor proposals -against RFP requirements and provides detailed scoring. -""" - -import os -import json -import time -from datetime import datetime -from typing import Annotated - -from openai import AzureOpenAI -from agent_framework.azure import AzureOpenAIChatClient, AzureOpenAIResponsesClient -from azure.identity import DefaultAzureCredential -from pydantic import BaseModel, Field -from dotenv import load_dotenv - -from .logging_config import get_logger - -load_dotenv() - -# Get logger from centralized config -logger = get_logger(__name__) - - -class RequirementScore(BaseModel): - """Score for a specific evaluation requirement.""" - requirement_id: str = Field(description="Requirement ID (e.g., 'I-1')") - requirement_name: str = Field(description="Name of the requirement") - requirement_text: str = Field(description="Description of what is being evaluated") - evaluation_stage: str = Field(description="Evaluation stage (e.g., 'Technical')") - target_value: str = Field(description="Target value if any, otherwise empty") - response_value: str = Field(description="What was found in the proposal") - maximum_score: int = Field(description="Maximum possible score (e.g., 20)") - score: float = Field(description="Actual score awarded (0 to maximum_score)") - weight: float = Field(description="Weight percentage (e.g., 14.0 for 14%)") - weighted_score: float = Field(description="Calculated weighted score") - comments: str = Field(description="Detailed justification for the score") - - -class EvaluationResult(BaseModel): - """Complete evaluation result structure matching RFP scoring format.""" - # Header Information - rfp_title: str = Field(description="Title of the RFP") - supplier_name: str = Field(description="Name of the vendor/supplier") - supplier_site: str = Field(description="Location/site of the supplier") - response_id: str = Field(description="Response/proposal ID") - - # Scoring Summary - scoring_status: str = Field(description="Status of scoring (e.g., 'Completed')") - requirement_score: float = Field(description="Total requirement score (sum of scores)") - composite_score: float = Field(description="Composite score including all weights") - overall_rank: int = Field(description="Ranking position") - - # Detailed Scores - requirements: list[RequirementScore] = Field(description="Scores for each requirement") - - # Analysis - strengths: list[str] = Field(description="Key strengths identified in the proposal") - weaknesses: list[str] = Field(description="Key weaknesses or gaps identified") - recommendations: list[str] = Field(description="Recommendations for the proposal") - summary: str = Field(description="Executive summary of the evaluation") - - -# Default scoring criteria if no guide is provided -DEFAULT_SCORING_GUIDE = """ -## Technical Evaluation Criteria (70% Weight) - -### I-1. Agency Reputation (20 points, Weight: 14%) -Evaluates the agency, the team dedicated for the Work, and Reference partners. - -### I-2. Methodology (20 points, Weight: 14%) -Evaluates methodology on the approach and the delivery timeline. - -### I-3. Themes (20 points, Weight: 14%) -Evaluates the list of proposed themes. - -### I-4. Structure (20 points, Weight: 14%) -Evaluates the structure of the proposed contents according to best practices. - -### I-5. Examples (20 points, Weight: 14%) -Evaluates examples of recent work done. - -## Score Calculation -- Each requirement scored 0-20 -- Weighted Score = (Score / 20) ร— Weight -- Total Technical Weight: 70% -""" - - -class ScoringAgent: - """ - AI Agent for evaluating vendor proposals against RFP requirements. - - Uses Azure OpenAI with o1/o3 reasoning models to perform intelligent - analysis and scoring of vendor proposals with deep reasoning capabilities. - """ - - def __init__(self): - """Initialize the scoring agent with Azure OpenAI reasoning model.""" - logger.info("[%s] Initializing ScoringAgent...", datetime.now().isoformat()) - init_start = time.time() - - # Validate required environment variables - self._validate_config() - - endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") - deployment_name = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME") - - # Create the Azure OpenAI client for reasoning models (o1/o3) - self.client = AzureOpenAIResponsesClient( - credential=DefaultAzureCredential(), - endpoint=endpoint, - deployment_name=deployment_name, - api_version="v1" - ) - - # Deployment name for the reasoning model (o3 or o1) - self.deployment_name = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME") - - init_duration = time.time() - init_start - logger.info("[%s] ScoringAgent initialized successfully in %.2fs", - datetime.now().isoformat(), init_duration) - - def _get_token_provider(self): - """Get Azure AD token provider for authentication.""" - from azure.identity import DefaultAzureCredential, get_bearer_token_provider - credential = DefaultAzureCredential() - return get_bearer_token_provider( - credential, "https://cognitiveservices.azure.com/.default" - ) - - def _validate_config(self): - """Validate required configuration.""" - required_vars = [ - "AZURE_OPENAI_ENDPOINT", - "AZURE_OPENAI_DEPLOYMENT_NAME", - ] - - missing = [var for var in required_vars if not os.getenv(var)] - - if missing: - raise ValueError( - f"Missing required environment variables: {', '.join(missing)}. " - "Please set them in your .env file." - ) - - async def evaluate( - self, - rfp_content: str, - proposal_content: str, - scoring_guide: str = "", - progress_callback=None, - reasoning_effort: str = "high" - ) -> dict: - """ - Evaluate a vendor proposal against RFP requirements using Azure OpenAI reasoning model. - - Args: - rfp_content: The RFP document content in markdown - proposal_content: The vendor proposal content in markdown - scoring_guide: Optional scoring guide/criteria in markdown - progress_callback: Optional callback function for progress updates - reasoning_effort: Reasoning effort level ("low", "medium", "high") - - Returns: - Dictionary containing evaluation results with timing metadata - """ - evaluate_start = time.time() - logger.info("[%s] Starting proposal evaluation (effort: %s)...", datetime.now().isoformat(), reasoning_effort) - logger.info("[%s] RFP content length: %d chars", datetime.now().isoformat(), len(rfp_content)) - logger.info("[%s] Proposal content length: %d chars", datetime.now().isoformat(), len(proposal_content)) - - # Use default guide if none provided - if not scoring_guide: - scoring_guide = DEFAULT_SCORING_GUIDE - logger.info("[%s] Using default scoring guide", datetime.now().isoformat()) - else: - logger.info("[%s] Using custom scoring guide (%d chars)", datetime.now().isoformat(), len(scoring_guide)) - - # Prepare the system instructions and user prompt - system_instructions = self._get_system_instructions(scoring_guide) - user_prompt = self._create_evaluation_prompt(rfp_content, proposal_content) - - logger.info("[%s] Sending request to Azure OpenAI reasoning model (deployment: %s, effort: %s)...", - datetime.now().isoformat(), self.deployment_name, reasoning_effort) - - if progress_callback: - progress_callback("Initializing AI reasoning engine...") - - # Call the reasoning model with specified reasoning effort - api_start = time.time() - try: - agent = self.client.create_agent( - instructions=system_instructions, - name="RFP Scoring Agent", - - additional_chat_options={"reasoning": {"effort": reasoning_effort, "summary": "detailed"}} - ) - agent_result = await agent.run(user_prompt) - - api_duration = time.time() - api_start - logger.info("[%s] Azure OpenAI API call completed in %.2fs", - datetime.now().isoformat(), api_duration) - - # Log token usage from AgentRunResponse.usage_details - usage = agent_result.usage_details - if usage: - input_tokens = usage.input_token_count or 0 - output_tokens = usage.output_token_count or 0 - total_tokens = usage.total_token_count or (input_tokens + output_tokens) - logger.info("[%s] Token usage - Input: %d, Output: %d, Total: %d", - datetime.now().isoformat(), - input_tokens, - output_tokens, - total_tokens) - - # AgentRunResponse.text concatenates all message text - response_text = agent_result.text - logger.info("[%s] Response received (%d chars)", datetime.now().isoformat(), len(response_text)) - - except Exception as e: - logger.error("[%s] Azure OpenAI API call failed: %s", datetime.now().isoformat(), str(e)) - raise - - # Parse the response - parse_start = time.time() - logger.info("[%s] Parsing evaluation response...", datetime.now().isoformat()) - result = self._parse_response(response_text) - parse_duration = time.time() - parse_start - logger.info("[%s] Response parsed in %.2fs", datetime.now().isoformat(), parse_duration) - - # Add timing metadata to result - total_duration = time.time() - evaluate_start - result["_metadata"] = { - "evaluation_timestamp": datetime.now().isoformat(), - "total_duration_seconds": round(total_duration, 2), - "api_call_duration_seconds": round(api_duration, 2), - "parse_duration_seconds": round(parse_duration, 2), - "model_deployment": self.deployment_name, - "reasoning_effort": reasoning_effort - } - - logger.info("[%s] Evaluation completed successfully in %.2fs", - datetime.now().isoformat(), total_duration) - - return result - - def _get_system_instructions(self, scoring_guide: str) -> str: - """Get system instructions for the evaluation agent.""" - return f"""You are an expert RFP (Request for Proposal) analyst and procurement specialist with extensive experience in evaluating vendor proposals for annual reports, creative services, and corporate communications projects. - -Your role is to perform a comprehensive, objective evaluation of vendor proposals against RFP requirements, producing a detailed scoring report in a standardized format. - -## YOUR EXPERTISE INCLUDES: -- Evaluating agency credentials, team qualifications, and reference partnerships -- Assessing project methodologies, timelines, and delivery approaches -- Analyzing creative themes, concepts, and innovative proposals -- Reviewing content structure and adherence to best practices -- Examining portfolio examples and past work quality -- Understanding industry benchmarks and competitive standards - -## EVALUATION FRAMEWORK - -{scoring_guide} - -## SCORING METHODOLOGY - -For each requirement, you must: - -1. **Analyze the RFP Requirement**: Identify what the RFP specifically asks for in each category -2. **Examine the Proposal Response**: Find and quote relevant sections from the vendor proposal -3. **Compare Against Best Practices**: Assess quality, completeness, and professionalism -4. **Assign a Score**: Score from 0-20 based on: - - 17-20 (Excellent): Exceeds requirements with exceptional quality - - 13-16 (Good): Fully meets requirements with good quality - - 9-12 (Adequate): Meets basic requirements with room for improvement - - 5-8 (Below Average): Partially meets requirements with significant gaps - - 0-4 (Poor): Fails to meet requirements or missing - -5. **Calculate Weighted Score**: weighted_score = (score / 20) ร— weight - -## EVALUATION REQUIREMENTS - -### I-1. Agency Reputation (Max: 20, Weight: 14%) -Evaluate: -- Agency's track record and industry standing -- Team credentials and relevant experience -- Quality and relevance of reference partners -- Client testimonials and case study outcomes -- Awards, certifications, and industry recognition - -### I-2. Methodology (Max: 20, Weight: 14%) -Evaluate: -- Clarity of proposed approach and work plan -- Feasibility and realism of delivery timeline -- Project management methodology -- Communication and reporting approach -- Risk identification and mitigation strategies - -### I-3. Themes (Max: 20, Weight: 14%) -Evaluate: -- Creativity and originality of proposed themes -- Alignment with company brand and values -- Relevance to project objectives -- Innovation and fresh perspective -- Coherence and storytelling potential - -### I-4. Structure (Max: 20, Weight: 14%) -Evaluate: -- Logical organization and flow -- Adherence to industry best practices -- Completeness of content coverage -- Visual hierarchy and presentation -- Accessibility and readability - -### I-5. Examples (Max: 20, Weight: 14%) -Evaluate: -- Quality of portfolio samples -- Relevance to current project -- Recency of work (last 2-3 years preferred) -- Diversity demonstrating range of capabilities -- Measurable outcomes achieved - -## OUTPUT FORMAT - -You MUST respond with a valid JSON object in the following exact structure: - -```json -{{ - "rfp_title": "Title extracted from the RFP document", - "supplier_name": "Vendor/company name from proposal", - "supplier_site": "Location/country from proposal", - "response_id": "Generate a unique ID like 'RESP-2025-XXXX'", - "scoring_status": "Completed", - "requirement_score": , - "composite_score": , - "overall_rank": 1, - "requirements": [ - {{ - "requirement_id": "I-1", - "requirement_name": "Agency Reputation", - "requirement_text": "The agency, the team dedicated for the Work, Reference partners.", - "evaluation_stage": "Technical", - "target_value": "", - "response_value": "Brief summary of what proposal offered", - "maximum_score": 20, - "score": <0-20>, - "weight": 14.0, - "weighted_score": , - "comments": "Detailed justification with specific references to proposal content" - }}, - {{ - "requirement_id": "I-2", - "requirement_name": "Methodology", - "requirement_text": "Methodology on the approach and the delivery timeline.", - "evaluation_stage": "Technical", - "target_value": "", - "response_value": "Brief summary of methodology proposed", - "maximum_score": 20, - "score": <0-20>, - "weight": 14.0, - "weighted_score": , - "comments": "Detailed justification" - }}, - {{ - "requirement_id": "I-3", - "requirement_name": "Themes", - "requirement_text": "A list of proposed themes.", - "evaluation_stage": "Technical", - "target_value": "", - "response_value": "Brief summary of themes proposed", - "maximum_score": 20, - "score": <0-20>, - "weight": 14.0, - "weighted_score": , - "comments": "Detailed justification" - }}, - {{ - "requirement_id": "I-4", - "requirement_name": "Structure", - "requirement_text": "The structure of the proposed contents according to best practices.", - "evaluation_stage": "Technical", - "target_value": "", - "response_value": "Brief summary of structure proposed", - "maximum_score": 20, - "score": <0-20>, - "weight": 14.0, - "weighted_score": , - "comments": "Detailed justification" - }}, - {{ - "requirement_id": "I-5", - "requirement_name": "Examples", - "requirement_text": "Examples of recent work done.", - "evaluation_stage": "Technical", - "target_value": "", - "response_value": "Brief summary of examples provided", - "maximum_score": 20, - "score": <0-20>, - "weight": 14.0, - "weighted_score": , - "comments": "Detailed justification" - }} - ], - "strengths": [ - "Specific strength 1 with reference to proposal", - "Specific strength 2 with reference to proposal", - "Specific strength 3 with reference to proposal" - ], - "weaknesses": [ - "Specific weakness/gap 1 with impact assessment", - "Specific weakness/gap 2 with impact assessment" - ], - "recommendations": [ - "Actionable recommendation 1", - "Actionable recommendation 2", - "Actionable recommendation 3" - ], - "summary": "A comprehensive 2-3 paragraph executive summary covering overall assessment, key findings, and final recommendation." -}} -``` - -## IMPORTANT GUIDELINES - -1. **Be Objective**: Base all scores on evidence found in the documents -2. **Be Specific**: Reference specific sections, quotes, or page numbers when possible -3. **Be Thorough**: Don't skip any evaluation category -4. **Calculate Accurately**: Ensure weighted_score = (score / maximum_score) ร— weight -5. **Provide Justification**: Every score must have detailed comments explaining the rationale -6. **Extract Metadata**: Pull rfp_title, supplier_name, supplier_site from the actual documents -7. **JSON Only**: Respond with ONLY the JSON object, no additional text or markdown formatting - -Remember: Your evaluation will be used for procurement decisions. Be fair, thorough, and professional.""" - - def _create_evaluation_prompt(self, rfp_content: str, proposal_content: str) -> str: - """Create the evaluation prompt.""" - return f"""Please evaluate the following vendor proposal against the RFP requirements. - -Perform a comprehensive analysis and score each requirement category as specified in your instructions. - -## RFP DOCUMENT -{rfp_content} - ---- - -## VENDOR PROPOSAL -{proposal_content} - ---- - -IMPORTANT REMINDERS: -1. Extract the RFP title from the RFP document -2. Extract the vendor/supplier name and location from the proposal -3. Score each of the 5 requirements (I-1 through I-5) on a scale of 0-20 -4. Calculate weighted scores: weighted_score = (score / 20) ร— 14.0 -5. Sum all scores for requirement_score (max 100) -6. Sum all weighted_scores for composite_score (max 70) -7. Provide detailed comments justifying each score -8. Include specific strengths, weaknesses, and recommendations - -Respond with ONLY a valid JSON object matching the exact schema in your instructions.""" - - def _parse_response(self, response_text: str) -> dict: - """Parse the agent response into a structured result.""" - try: - # Try to extract JSON from the response - # Handle cases where the response might have markdown code blocks - text = response_text.strip() - - # Remove markdown code blocks if present - if text.startswith("```json"): - text = text[7:] - elif text.startswith("```"): - text = text[3:] - - if text.endswith("```"): - text = text[:-3] - - text = text.strip() - - # Parse JSON - result = json.loads(text) - - # Return the new format structure - return { - "rfp_title": result.get("rfp_title", "RFP Evaluation"), - "supplier_name": result.get("supplier_name", "Unknown Vendor"), - "supplier_site": result.get("supplier_site", ""), - "response_id": result.get("response_id", "RESP-0000"), - "scoring_status": result.get("scoring_status", "Completed"), - "requirement_score": result.get("requirement_score", 0), - "composite_score": result.get("composite_score", 0), - "overall_rank": result.get("overall_rank", 1), - "requirements": result.get("requirements", []), - "strengths": result.get("strengths", []), - "weaknesses": result.get("weaknesses", []), - "recommendations": result.get("recommendations", []), - "summary": result.get("summary", ""), - } - - except json.JSONDecodeError: - # If JSON parsing fails, return a default structure with the raw response - return { - "rfp_title": "RFP Evaluation", - "supplier_name": "Unknown Vendor", - "supplier_site": "", - "response_id": "RESP-ERROR", - "scoring_status": "Error", - "requirement_score": 0, - "composite_score": 0, - "overall_rank": 0, - "requirements": [], - "strengths": [], - "weaknesses": [], - "recommendations": [], - "summary": f"Error parsing evaluation results. Raw response: {response_text[:500]}...", - } diff --git a/app/services/scoring_agent_v2.py b/app/services/scoring_agent_v2.py deleted file mode 100644 index 04ffa60..0000000 --- a/app/services/scoring_agent_v2.py +++ /dev/null @@ -1,732 +0,0 @@ -""" -RFP Scoring Agent V2 - Multi-Agent Architecture. - -This module implements a multi-agent system for RFP evaluation: -1. Criteria Extraction Agent - Extracts scoring criteria from RFP with weights -2. Proposal Scoring Agent - Scores the proposal against extracted criteria -""" - -import os -import json -import time -from datetime import datetime -from typing import Callable, Optional - -from agent_framework.azure import AzureOpenAIResponsesClient -from azure.identity import DefaultAzureCredential -from pydantic import BaseModel, Field -from dotenv import load_dotenv - -from .logging_config import get_logger - -load_dotenv() - -# Get logger from centralized config -logger = get_logger(__name__) - - -# ============================================================================ -# Pydantic Models for V2 Scoring -# ============================================================================ - -class ScoringCriterion(BaseModel): - """A single scoring criterion extracted from the RFP.""" - criterion_id: str = Field(description="Unique ID for the criterion (e.g., 'C-1')") - name: str = Field(description="Name of the criterion") - description: str = Field(description="Detailed description of what is being evaluated") - category: str = Field(description="Category (e.g., 'Technical', 'Financial', 'Experience')") - weight: float = Field(description="Weight percentage (all weights must sum to 100)") - max_score: int = Field(default=100, description="Maximum score for this criterion") - evaluation_guidance: str = Field(description="Guidance on how to evaluate this criterion") - - -class ExtractedCriteria(BaseModel): - """Complete set of extracted scoring criteria from an RFP.""" - rfp_title: str = Field(description="Title of the RFP") - rfp_summary: str = Field(description="Brief summary of what the RFP is requesting") - total_weight: float = Field(default=100.0, description="Total weight (should be 100)") - criteria: list[ScoringCriterion] = Field(description="List of scoring criteria") - extraction_notes: str = Field(description="Notes about the extraction process") - - -class CriterionScore(BaseModel): - """Score for a single criterion.""" - criterion_id: str = Field(description="ID of the criterion being scored") - criterion_name: str = Field(description="Name of the criterion") - weight: float = Field(description="Weight percentage") - raw_score: float = Field(description="Raw score (0-100)") - weighted_score: float = Field(description="Weighted score (raw_score * weight / 100)") - evidence: str = Field(description="Evidence from the proposal supporting this score") - justification: str = Field(description="Detailed justification for the score") - strengths: list[str] = Field(description="Specific strengths for this criterion") - gaps: list[str] = Field(description="Specific gaps or weaknesses for this criterion") - - -class ProposalEvaluationV2(BaseModel): - """Complete V2 evaluation result.""" - # Header Information - rfp_title: str = Field(description="Title of the RFP") - supplier_name: str = Field(description="Name of the vendor/supplier") - supplier_site: str = Field(description="Location/site of the supplier") - response_id: str = Field(description="Response/proposal ID") - evaluation_date: str = Field(description="Date of evaluation") - - # Scoring Summary - total_score: float = Field(description="Total weighted score (0-100)") - score_percentage: float = Field(description="Score as percentage") - grade: str = Field(description="Letter grade (A, B, C, D, F)") - recommendation: str = Field(description="Overall recommendation") - - # Detailed Scores - criterion_scores: list[CriterionScore] = Field(description="Scores for each criterion") - - # Analysis - executive_summary: str = Field(description="Executive summary of the evaluation") - overall_strengths: list[str] = Field(description="Key strengths across all criteria") - overall_weaknesses: list[str] = Field(description="Key weaknesses across all criteria") - recommendations: list[str] = Field(description="Actionable recommendations") - risk_assessment: str = Field(description="Assessment of risks with this vendor") - - -# ============================================================================ -# Agent 1: Criteria Extraction Agent -# ============================================================================ - -class CriteriaExtractionAgent: - """ - Agent responsible for extracting scoring criteria from an RFP document. - - Analyzes the RFP to identify evaluation criteria, assigns weights, - and provides guidance for scoring each criterion. - """ - - SYSTEM_INSTRUCTIONS = """You are an expert procurement analyst specializing in RFP (Request for Proposal) analysis. - -Your task is to carefully analyze RFP documents and extract comprehensive scoring criteria that will be used to evaluate vendor proposals. - -## YOUR RESPONSIBILITIES: - -1. **Identify Evaluation Criteria**: Find all evaluation criteria mentioned in the RFP, including: - - Explicitly stated criteria (often in "Evaluation Criteria" or "Selection Criteria" sections) - - Implied criteria based on requirements and priorities - - Industry-standard criteria relevant to the type of work - -2. **Assign Weights**: Distribute 100 total weight points across criteria based on: - - Explicit weights mentioned in the RFP - - Emphasis and priority indicated in the document - - Industry standards for similar projects - - Balanced evaluation across technical, financial, and qualitative factors - -3. **Provide Evaluation Guidance**: For each criterion, explain: - - What constitutes excellent performance (90-100 score) - - What constitutes good performance (70-89 score) - - What constitutes acceptable performance (50-69 score) - - What constitutes poor performance (below 50) - -## WEIGHT DISTRIBUTION GUIDELINES: - -- Technical capabilities: typically 30-50% -- Experience and track record: typically 15-25% -- Methodology and approach: typically 15-25% -- Pricing/value: typically 15-30% (if mentioned) -- Team qualifications: typically 10-20% - -## OUTPUT REQUIREMENTS: - -You MUST respond with a valid JSON object matching this exact structure: - -```json -{ - "rfp_title": "Extracted RFP title", - "rfp_summary": "2-3 sentence summary of what the RFP is requesting", - "total_weight": 100.0, - "criteria": [ - { - "criterion_id": "C-1", - "name": "Criterion Name", - "description": "Detailed description of what this criterion evaluates", - "category": "Technical|Financial|Experience|Qualitative", - "weight": , - "max_score": 100, - "evaluation_guidance": "Detailed guidance on how to score this criterion" - } - ], - "extraction_notes": "Notes about how criteria were identified and weighted" -} -``` - -## IMPORTANT: -- All weights MUST sum to exactly 100 -- Include at least 4-8 meaningful criteria -- Be specific in descriptions and guidance -- Respond with ONLY valid JSON, no additional text""" - - def __init__(self): - """Initialize the criteria extraction agent.""" - logger.info("Initializing CriteriaExtractionAgent...") - - self._validate_config() - - endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") - deployment_name = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME") - - self.client = AzureOpenAIResponsesClient( - credential=DefaultAzureCredential(), - endpoint=endpoint, - deployment_name=deployment_name, - api_version="v1" - ) - - self.deployment_name = deployment_name - logger.info("CriteriaExtractionAgent initialized") - - def _validate_config(self): - """Validate required configuration.""" - required_vars = ["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOYMENT_NAME"] - missing = [var for var in required_vars if not os.getenv(var)] - if missing: - raise ValueError(f"Missing required environment variables: {', '.join(missing)}") - - async def extract_criteria( - self, - rfp_content: str, - progress_callback: Optional[Callable[[str], None]] = None, - reasoning_effort: str = "high" - ) -> ExtractedCriteria: - """ - Extract scoring criteria from the RFP document. - - Args: - rfp_content: The RFP document content in markdown - progress_callback: Optional callback for progress updates - reasoning_effort: Reasoning effort level ("low", "medium", "high") - - Returns: - ExtractedCriteria object with all identified criteria - """ - start_time = time.time() - logger.info("Starting criteria extraction (effort: %s)...", reasoning_effort) - - if progress_callback: - progress_callback("Analyzing RFP structure and requirements...") - - user_prompt = f"""Please analyze the following RFP document and extract comprehensive scoring criteria. - -## RFP DOCUMENT: - -{rfp_content} - ---- - -REQUIREMENTS: -1. Identify all evaluation criteria (explicit and implied) -2. Assign weights that sum to exactly 100 -3. Provide detailed evaluation guidance for each criterion -4. Include the RFP title and a brief summary - -Respond with ONLY valid JSON matching the schema in your instructions.""" - - try: - agent = self.client.create_agent( - instructions=self.SYSTEM_INSTRUCTIONS, - name="Criteria Extraction Agent", - additional_chat_options={"reasoning": {"effort": reasoning_effort, "summary": "detailed"}} - ) - - if progress_callback: - progress_callback("Extracting and analyzing criteria...") - - result = await agent.run(user_prompt) - response_text = result.text - - # Log token usage - usage = result.usage_details - if usage: - logger.info("Criteria extraction - Tokens: Input=%d, Output=%d, Total=%d", - usage.input_token_count or 0, - usage.output_token_count or 0, - usage.total_token_count or 0) - - # Parse the response - criteria_data = self._parse_response(response_text) - - duration = time.time() - start_time - logger.info("Criteria extraction completed in %.2fs - Found %d criteria", - duration, len(criteria_data.get("criteria", []))) - - return ExtractedCriteria(**criteria_data) - - except Exception as e: - logger.error("Criteria extraction failed: %s", str(e)) - raise - - def _parse_response(self, response_text: str) -> dict: - """Parse the agent response into a dictionary.""" - text = response_text.strip() - - # Remove markdown code blocks - if text.startswith("```json"): - text = text[7:] - elif text.startswith("```"): - text = text[3:] - if text.endswith("```"): - text = text[:-3] - text = text.strip() - - try: - data = json.loads(text) - - # Validate and normalize weights - criteria = data.get("criteria", []) - total_weight = sum(c.get("weight", 0) for c in criteria) - - # Normalize if weights don't sum to 100 - if criteria and abs(total_weight - 100) > 0.1: - logger.warning("Normalizing weights from %.2f to 100", total_weight) - for criterion in criteria: - criterion["weight"] = (criterion.get("weight", 0) / total_weight) * 100 - data["total_weight"] = 100.0 - - return data - - except json.JSONDecodeError as e: - logger.error("Failed to parse criteria JSON: %s", str(e)) - # Return default structure - return { - "rfp_title": "Unknown RFP", - "rfp_summary": "Failed to extract RFP summary", - "total_weight": 100.0, - "criteria": [], - "extraction_notes": f"Error parsing response: {str(e)}" - } - - -# ============================================================================ -# Agent 2: Proposal Scoring Agent -# ============================================================================ - -class ProposalScoringAgent: - """ - Agent responsible for scoring a proposal against extracted criteria. - - Takes the criteria from Agent 1 and evaluates the vendor proposal, - providing detailed scores and justifications. - """ - - SYSTEM_INSTRUCTIONS_TEMPLATE = """You are an expert procurement evaluator with extensive experience scoring vendor proposals. - -Your task is to objectively evaluate a vendor proposal against specific scoring criteria extracted from an RFP. - -## SCORING METHODOLOGY: - -For EACH criterion, you must: - -1. **Find Evidence**: Locate relevant content in the proposal -2. **Assess Quality**: Compare against the evaluation guidance -3. **Assign Score**: Score 0-100 based on: - - 90-100 (Excellent): Exceeds requirements, exceptional quality - - 70-89 (Good): Fully meets requirements, high quality - - 50-69 (Acceptable): Meets minimum requirements - - 30-49 (Below Average): Partially meets requirements - - 0-29 (Poor): Fails to meet requirements - -4. **Calculate Weighted Score**: weighted_score = (raw_score * weight) / 100 - -5. **Document Everything**: Provide evidence, justification, strengths, and gaps - -## EVALUATION CRITERIA TO USE: - -{criteria_json} - -## GRADE ASSIGNMENT: - -Based on total weighted score: -- A: 90-100 -- B: 80-89 -- C: 70-79 -- D: 60-69 -- F: Below 60 - -## OUTPUT FORMAT: - -You MUST respond with a valid JSON object: - -```json -{{ - "rfp_title": "RFP title", - "supplier_name": "Extracted vendor name", - "supplier_site": "Vendor location", - "response_id": "Generate ID like RESP-2025-XXXX", - "evaluation_date": "YYYY-MM-DD", - "total_score": , - "score_percentage": , - "grade": "A/B/C/D/F", - "recommendation": "Clear recommendation statement", - "criterion_scores": [ - {{ - "criterion_id": "C-1", - "criterion_name": "Criterion Name", - "weight": , - "raw_score": <0-100>, - "weighted_score": , - "evidence": "Specific evidence from proposal", - "justification": "Detailed scoring justification", - "strengths": ["strength1", "strength2"], - "gaps": ["gap1", "gap2"] - }} - ], - "executive_summary": "2-3 paragraph executive summary", - "overall_strengths": ["key strength 1", "key strength 2"], - "overall_weaknesses": ["key weakness 1", "key weakness 2"], - "recommendations": ["recommendation 1", "recommendation 2"], - "risk_assessment": "Assessment of risks with this vendor" -}} -``` - -## IMPORTANT: -- Score EVERY criterion from the provided list -- Provide specific evidence from the proposal -- Be objective and fair -- Respond with ONLY valid JSON""" - - def __init__(self): - """Initialize the proposal scoring agent.""" - logger.info("Initializing ProposalScoringAgent...") - - self._validate_config() - - endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") - deployment_name = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME") - - self.client = AzureOpenAIResponsesClient( - credential=DefaultAzureCredential(), - endpoint=endpoint, - deployment_name=deployment_name, - api_version="v1" - ) - - self.deployment_name = deployment_name - logger.info("ProposalScoringAgent initialized") - - def _validate_config(self): - """Validate required configuration.""" - required_vars = ["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOYMENT_NAME"] - missing = [var for var in required_vars if not os.getenv(var)] - if missing: - raise ValueError(f"Missing required environment variables: {', '.join(missing)}") - - async def score_proposal( - self, - criteria: ExtractedCriteria, - proposal_content: str, - progress_callback: Optional[Callable[[str], None]] = None, - reasoning_effort: str = "high" - ) -> ProposalEvaluationV2: - """ - Score the proposal against extracted criteria. - - Args: - criteria: ExtractedCriteria from the extraction agent - proposal_content: The vendor proposal content - progress_callback: Optional callback for progress updates - reasoning_effort: Reasoning effort level ("low", "medium", "high") - - Returns: - ProposalEvaluationV2 with complete scoring results - """ - start_time = time.time() - logger.info("Starting proposal scoring against %d criteria (effort: %s)...", - len(criteria.criteria), reasoning_effort) - - if progress_callback: - progress_callback("Preparing scoring framework...") - - # Format criteria for the system instructions - criteria_json = json.dumps([ - { - "criterion_id": c.criterion_id, - "name": c.name, - "description": c.description, - "category": c.category, - "weight": c.weight, - "max_score": c.max_score, - "evaluation_guidance": c.evaluation_guidance - } - for c in criteria.criteria - ], indent=2) - - system_instructions = self.SYSTEM_INSTRUCTIONS_TEMPLATE.format( - criteria_json=criteria_json - ) - - user_prompt = f"""Please evaluate the following vendor proposal against the scoring criteria. - -## RFP CONTEXT: -- Title: {criteria.rfp_title} -- Summary: {criteria.rfp_summary} - -## VENDOR PROPOSAL: - -{proposal_content} - ---- - -REQUIREMENTS: -1. Score each criterion from 0-100 -2. Calculate weighted scores -3. Provide evidence and justification for each score -4. Summarize strengths, weaknesses, and recommendations -5. Assign an overall grade - -Respond with ONLY valid JSON matching the schema in your instructions.""" - - try: - agent = self.client.create_agent( - instructions=system_instructions, - name="Proposal Scoring Agent", - additional_chat_options={"reasoning": {"effort": reasoning_effort, "summary": "detailed"}} - ) - - if progress_callback: - progress_callback("Scoring proposal against criteria...") - - result = await agent.run(user_prompt) - response_text = result.text - - # Log token usage - usage = result.usage_details - if usage: - logger.info("Proposal scoring - Tokens: Input=%d, Output=%d, Total=%d", - usage.input_token_count or 0, - usage.output_token_count or 0, - usage.total_token_count or 0) - - # Parse the response - evaluation_data = self._parse_response(response_text, criteria) - - duration = time.time() - start_time - logger.info("Proposal scoring completed in %.2fs - Total score: %.2f", - duration, evaluation_data.get("total_score", 0)) - - return ProposalEvaluationV2(**evaluation_data) - - except Exception as e: - logger.error("Proposal scoring failed: %s", str(e)) - raise - - def _parse_response(self, response_text: str, criteria: ExtractedCriteria) -> dict: - """Parse the agent response into a dictionary.""" - text = response_text.strip() - - # Remove markdown code blocks - if text.startswith("```json"): - text = text[7:] - elif text.startswith("```"): - text = text[3:] - if text.endswith("```"): - text = text[:-3] - text = text.strip() - - try: - data = json.loads(text) - - # Ensure evaluation_date is set - if not data.get("evaluation_date"): - data["evaluation_date"] = datetime.now().strftime("%Y-%m-%d") - - # Recalculate total score for accuracy - criterion_scores = data.get("criterion_scores", []) - total_score = sum(cs.get("weighted_score", 0) for cs in criterion_scores) - data["total_score"] = round(total_score, 2) - data["score_percentage"] = round(total_score, 2) - - # Determine grade - if total_score >= 90: - data["grade"] = "A" - elif total_score >= 80: - data["grade"] = "B" - elif total_score >= 70: - data["grade"] = "C" - elif total_score >= 60: - data["grade"] = "D" - else: - data["grade"] = "F" - - return data - - except json.JSONDecodeError as e: - logger.error("Failed to parse scoring JSON: %s", str(e)) - # Return default structure - return { - "rfp_title": criteria.rfp_title, - "supplier_name": "Unknown Vendor", - "supplier_site": "Unknown", - "response_id": "RESP-ERROR", - "evaluation_date": datetime.now().strftime("%Y-%m-%d"), - "total_score": 0, - "score_percentage": 0, - "grade": "F", - "recommendation": "Unable to complete evaluation due to parsing error", - "criterion_scores": [], - "executive_summary": f"Error parsing evaluation: {str(e)}", - "overall_strengths": [], - "overall_weaknesses": [], - "recommendations": [], - "risk_assessment": "Unable to assess" - } - - -# ============================================================================ -# Main V2 Scoring Orchestrator -# ============================================================================ - -class ScoringAgentV2: - """ - Multi-Agent RFP Scoring System V2. - - Orchestrates two agents: - 1. CriteriaExtractionAgent - Extracts scoring criteria from RFP - 2. ProposalScoringAgent - Scores proposal against criteria - """ - - def __init__(self): - """Initialize the V2 scoring system.""" - logger.info("Initializing ScoringAgentV2 (Multi-Agent System)...") - - self.criteria_agent = CriteriaExtractionAgent() - self.scoring_agent = ProposalScoringAgent() - - logger.info("ScoringAgentV2 initialized with 2 agents") - - async def evaluate( - self, - rfp_content: str, - proposal_content: str, - scoring_guide: str = "", # Kept for API compatibility but not used in v2 - progress_callback: Optional[Callable[[str], None]] = None, - reasoning_effort: str = "high" - ) -> dict: - """ - Perform full multi-agent evaluation. - - Args: - rfp_content: The RFP document content - proposal_content: The vendor proposal content - scoring_guide: (Unused in V2 - criteria extracted from RFP) - progress_callback: Optional callback for progress updates - reasoning_effort: Reasoning effort level ("low", "medium", "high") - - Returns: - Dictionary containing complete evaluation results with metadata - """ - total_start = time.time() - logger.info("====== V2 MULTI-AGENT EVALUATION STARTED (effort: %s) ======", reasoning_effort) - - # Phase 1: Extract criteria from RFP - phase1_start = time.time() - if progress_callback: - progress_callback("Phase 1: Extracting scoring criteria from RFP...") - - criteria = await self.criteria_agent.extract_criteria( - rfp_content, - progress_callback=progress_callback, - reasoning_effort=reasoning_effort - ) - phase1_duration = time.time() - phase1_start - - logger.info("Phase 1 completed in %.2fs - Extracted %d criteria", - phase1_duration, len(criteria.criteria)) - - # Phase 2: Score the proposal - phase2_start = time.time() - if progress_callback: - progress_callback("Phase 2: Scoring proposal against extracted criteria...") - - evaluation = await self.scoring_agent.score_proposal( - criteria, - proposal_content, - progress_callback=progress_callback, - reasoning_effort=reasoning_effort - ) - phase2_duration = time.time() - phase2_start - - logger.info("Phase 2 completed in %.2fs - Total score: %.2f", - phase2_duration, evaluation.total_score) - - # Compile final results - total_duration = time.time() - total_start - - results = { - # Header info (compatible with V1 format where possible) - "rfp_title": evaluation.rfp_title, - "supplier_name": evaluation.supplier_name, - "supplier_site": evaluation.supplier_site, - "response_id": evaluation.response_id, - "evaluation_date": evaluation.evaluation_date, - - # V2 specific scoring - "total_score": evaluation.total_score, - "score_percentage": evaluation.score_percentage, - "grade": evaluation.grade, - "recommendation": evaluation.recommendation, - - # Criteria and scores - "extracted_criteria": { - "rfp_summary": criteria.rfp_summary, - "total_weight": criteria.total_weight, - "criteria_count": len(criteria.criteria), - "criteria": [ - { - "criterion_id": c.criterion_id, - "name": c.name, - "description": c.description, - "category": c.category, - "weight": c.weight, - "evaluation_guidance": c.evaluation_guidance - } - for c in criteria.criteria - ], - "extraction_notes": criteria.extraction_notes - }, - - "criterion_scores": [ - { - "criterion_id": cs.criterion_id, - "criterion_name": cs.criterion_name, - "weight": cs.weight, - "raw_score": cs.raw_score, - "weighted_score": cs.weighted_score, - "evidence": cs.evidence, - "justification": cs.justification, - "strengths": cs.strengths, - "gaps": cs.gaps - } - for cs in evaluation.criterion_scores - ], - - # Analysis - "executive_summary": evaluation.executive_summary, - "overall_strengths": evaluation.overall_strengths, - "overall_weaknesses": evaluation.overall_weaknesses, - "recommendations": evaluation.recommendations, - "risk_assessment": evaluation.risk_assessment, - - # Metadata - "_metadata": { - "version": "2.0", - "evaluation_type": "multi-agent", - "evaluation_timestamp": datetime.now().isoformat(), - "total_duration_seconds": round(total_duration, 2), - "phase1_criteria_extraction_seconds": round(phase1_duration, 2), - "phase2_proposal_scoring_seconds": round(phase2_duration, 2), - "criteria_count": len(criteria.criteria), - "model_deployment": self.criteria_agent.deployment_name, - "reasoning_effort": reasoning_effort - } - } - - logger.info("====== V2 MULTI-AGENT EVALUATION COMPLETED ======") - logger.info("Total duration: %.2fs (Phase 1: %.2fs, Phase 2: %.2fs)", - total_duration, phase1_duration, phase2_duration) - - return results diff --git a/app/tests/RfpAnalyzer.Tests/Models/ComparisonModelsTests.cs b/app/tests/RfpAnalyzer.Tests/Models/ComparisonModelsTests.cs new file mode 100644 index 0000000..9e3cba9 --- /dev/null +++ b/app/tests/RfpAnalyzer.Tests/Models/ComparisonModelsTests.cs @@ -0,0 +1,38 @@ +using RfpAnalyzer.Models; + +namespace RfpAnalyzer.Tests.Models; + +public class ComparisonModelsTests +{ + [Fact] + public void ComparisonResult_DefaultValues() + { + var result = new ComparisonResult(); + Assert.Equal("", result.RfpTitle); + Assert.Equal(0, result.TotalVendors); + Assert.Empty(result.VendorRankings); + Assert.Empty(result.CriterionComparisons); + Assert.Empty(result.ComparisonInsights); + } + + [Fact] + public void VendorRanking_DefaultValues() + { + var ranking = new VendorRanking(); + Assert.Equal(0, ranking.Rank); + Assert.Equal("", ranking.VendorName); + Assert.Equal(0, ranking.TotalScore); + Assert.Empty(ranking.KeyStrengths); + Assert.Empty(ranking.KeyConcerns); + } + + [Fact] + public void CriterionComparison_DefaultValues() + { + var comp = new CriterionComparison(); + Assert.Equal("", comp.CriterionId); + Assert.Equal("", comp.BestVendor); + Assert.Equal("", comp.WorstVendor); + Assert.Equal(0, comp.Weight); + } +} diff --git a/app/tests/RfpAnalyzer.Tests/Models/DurationFormatterTests.cs b/app/tests/RfpAnalyzer.Tests/Models/DurationFormatterTests.cs new file mode 100644 index 0000000..c0907d8 --- /dev/null +++ b/app/tests/RfpAnalyzer.Tests/Models/DurationFormatterTests.cs @@ -0,0 +1,33 @@ +using RfpAnalyzer.Models; + +namespace RfpAnalyzer.Tests.Models; + +public class DurationFormatterTests +{ + [Theory] + [InlineData(0.5, "500ms")] + [InlineData(0.001, "1ms")] + [InlineData(0.999, "999ms")] + public void Format_SubSecond_ReturnsMilliseconds(double seconds, string expected) + { + Assert.Equal(expected, DurationFormatter.Format(seconds)); + } + + [Theory] + [InlineData(1.0, "1.0s")] + [InlineData(5.5, "5.5s")] + [InlineData(59.9, "59.9s")] + public void Format_Seconds_ReturnsSeconds(double seconds, string expected) + { + Assert.Equal(expected, DurationFormatter.Format(seconds)); + } + + [Theory] + [InlineData(60, "1m 0.0s")] + [InlineData(90, "1m 30.0s")] + [InlineData(125.5, "2m 5.5s")] + public void Format_Minutes_ReturnsMinutesAndSeconds(double seconds, string expected) + { + Assert.Equal(expected, DurationFormatter.Format(seconds)); + } +} diff --git a/app/tests/RfpAnalyzer.Tests/Models/EvaluationModelsTests.cs b/app/tests/RfpAnalyzer.Tests/Models/EvaluationModelsTests.cs new file mode 100644 index 0000000..32f7772 --- /dev/null +++ b/app/tests/RfpAnalyzer.Tests/Models/EvaluationModelsTests.cs @@ -0,0 +1,49 @@ +using RfpAnalyzer.Models; + +namespace RfpAnalyzer.Tests.Models; + +public class EvaluationModelsTests +{ + [Fact] + public void EvaluationResult_DefaultValues() + { + var result = new EvaluationResult(); + Assert.Equal("", result.RfpTitle); + Assert.Equal("", result.SupplierName); + Assert.Equal(0, result.TotalScore); + Assert.Empty(result.CriterionScores); + Assert.Null(result.Metadata); + } + + [Fact] + public void UploadedDocument_DefaultValues() + { + var doc = new UploadedDocument(); + Assert.Equal("", doc.FileName); + Assert.Empty(doc.Content); + Assert.False(doc.IsExtracted); + Assert.Null(doc.ExtractedContent); + } + + [Fact] + public void AppState_DefaultValues() + { + var state = new AppState(); + Assert.Null(state.RfpDocument); + Assert.Empty(state.ProposalDocuments); + Assert.Equal(ExtractionService.ContentUnderstanding, state.SelectedService); + Assert.Equal("high", state.ReasoningEffort); + Assert.Empty(state.EvaluationResults); + Assert.Null(state.ComparisonResult); + Assert.Equal(0, state.CurrentStep); + } + + [Fact] + public void EvaluationMetadata_DefaultValues() + { + var meta = new EvaluationMetadata(); + Assert.Equal("2.0", meta.Version); + Assert.Equal("multi-agent", meta.EvaluationType); + Assert.Equal(0, meta.TotalDurationSeconds); + } +} diff --git a/app/tests/RfpAnalyzer.Tests/Models/ProcessingQueueTests.cs b/app/tests/RfpAnalyzer.Tests/Models/ProcessingQueueTests.cs new file mode 100644 index 0000000..e714733 --- /dev/null +++ b/app/tests/RfpAnalyzer.Tests/Models/ProcessingQueueTests.cs @@ -0,0 +1,187 @@ +using RfpAnalyzer.Models; + +namespace RfpAnalyzer.Tests.Models; + +public class ProcessingQueueTests +{ + [Fact] + public void QueueItem_Start_SetsProcessingStatus() + { + var item = new QueueItem { Id = "1", Name = "Test", ItemType = "test" }; + item.Start(); + Assert.Equal(QueueItemStatus.Processing, item.Status); + Assert.NotNull(item.StartTime); + } + + [Fact] + public void QueueItem_Complete_SetsCompletedStatusAndDuration() + { + var item = new QueueItem { Id = "1", Name = "Test", ItemType = "test" }; + item.Start(); + Thread.Sleep(10); + item.Complete("result"); + Assert.Equal(QueueItemStatus.Completed, item.Status); + Assert.NotNull(item.EndTime); + Assert.NotNull(item.Duration); + Assert.True(item.Duration > 0); + Assert.Equal("result", item.Result); + } + + [Fact] + public void QueueItem_Fail_SetsFailedStatusAndError() + { + var item = new QueueItem { Id = "1", Name = "Test", ItemType = "test" }; + item.Start(); + item.Fail("Something went wrong"); + Assert.Equal(QueueItemStatus.Failed, item.Status); + Assert.Equal("Something went wrong", item.ErrorMessage); + } + + [Fact] + public void QueueItem_GetStatusIcon_ReturnsCorrectIcons() + { + var pending = new QueueItem { Id = "1", Name = "T", ItemType = "t" }; + Assert.Equal("โณ", pending.GetStatusIcon()); + + pending.Start(); + Assert.Equal("๐Ÿ”„", pending.GetStatusIcon()); + + pending.Complete(); + Assert.Equal("โœ…", pending.GetStatusIcon()); + + var failed = new QueueItem { Id = "2", Name = "T", ItemType = "t" }; + failed.Start(); + failed.Fail("err"); + Assert.Equal("โŒ", failed.GetStatusIcon()); + } + + [Fact] + public void QueueItem_GetElapsedTime_ReturnsZeroWhenNotStarted() + { + var item = new QueueItem { Id = "1", Name = "T", ItemType = "t" }; + Assert.Equal(0.0, item.GetElapsedTime()); + } + + [Fact] + public void QueueItem_GetElapsedTime_ReturnsDurationWhenCompleted() + { + var item = new QueueItem { Id = "1", Name = "T", ItemType = "t" }; + item.Start(); + Thread.Sleep(50); + item.Complete(); + Assert.True(item.GetElapsedTime() > 0); + } + + [Fact] + public void ProcessingQueue_AddItem_AddsToList() + { + var queue = new ProcessingQueue { Name = "Test" }; + var item = queue.AddItem("1", "Test Item", "test"); + Assert.Single(queue.Items); + Assert.Equal("1", item.Id); + Assert.Equal("Test Item", item.Name); + Assert.Equal("test", item.ItemType); + } + + [Fact] + public void ProcessingQueue_GetItem_FindsById() + { + var queue = new ProcessingQueue { Name = "Test" }; + queue.AddItem("1", "First", "test"); + queue.AddItem("2", "Second", "test"); + + var found = queue.GetItem("2"); + Assert.NotNull(found); + Assert.Equal("Second", found.Name); + + var notFound = queue.GetItem("99"); + Assert.Null(notFound); + } + + [Fact] + public void ProcessingQueue_GetProgress_ReturnsCorrectCounts() + { + var queue = new ProcessingQueue { Name = "Test" }; + var item1 = queue.AddItem("1", "A", "test"); + var item2 = queue.AddItem("2", "B", "test"); + var item3 = queue.AddItem("3", "C", "test"); + var item4 = queue.AddItem("4", "D", "test"); + + item1.Start(); + item1.Complete(); + item2.Start(); + item3.Start(); + item3.Fail("err"); + + var progress = queue.GetProgress(); + Assert.Equal(4, progress.Total); + Assert.Equal(1, progress.Completed); + Assert.Equal(1, progress.Failed); + Assert.Equal(1, progress.Processing); + Assert.Equal(1, progress.Pending); + Assert.Equal(50, progress.Percentage); // (1 completed + 1 failed) / 4 = 50% + } + + [Fact] + public void ProcessingQueue_IsComplete_TrueWhenAllDone() + { + var queue = new ProcessingQueue { Name = "Test" }; + var item1 = queue.AddItem("1", "A", "test"); + var item2 = queue.AddItem("2", "B", "test"); + + Assert.False(queue.IsComplete); + + item1.Start(); + item1.Complete(); + Assert.False(queue.IsComplete); + + item2.Start(); + item2.Fail("err"); + Assert.True(queue.IsComplete); + } + + [Fact] + public void ProcessingQueue_Clear_RemovesAllItems() + { + var queue = new ProcessingQueue { Name = "Test" }; + queue.Start(); + queue.AddItem("1", "A", "test"); + queue.AddItem("2", "B", "test"); + + queue.Clear(); + Assert.Empty(queue.Items); + Assert.Null(queue.StartTime); + Assert.Null(queue.EndTime); + } + + [Fact] + public void ProcessingQueue_GetTotalDuration_ReturnsZeroWhenNotStarted() + { + var queue = new ProcessingQueue { Name = "Test" }; + Assert.Equal(0.0, queue.GetTotalDuration()); + } + + [Fact] + public void ProcessingQueue_GetAverageItemDuration_ReturnsZeroWithNoCompleted() + { + var queue = new ProcessingQueue { Name = "Test" }; + queue.AddItem("1", "A", "test"); + Assert.Equal(0.0, queue.GetAverageItemDuration()); + } + + [Fact] + public void ProcessingQueue_GetPendingCompletedFailed_FiltersCorrectly() + { + var queue = new ProcessingQueue { Name = "Test" }; + var a = queue.AddItem("1", "A", "test"); + var b = queue.AddItem("2", "B", "test"); + var c = queue.AddItem("3", "C", "test"); + + a.Start(); a.Complete(); + b.Start(); b.Fail("err"); + + Assert.Single(queue.GetCompletedItems()); + Assert.Single(queue.GetFailedItems()); + Assert.Single(queue.GetPendingItems()); + } +} diff --git a/app/tests/RfpAnalyzer.Tests/Models/ScoringModelsTests.cs b/app/tests/RfpAnalyzer.Tests/Models/ScoringModelsTests.cs new file mode 100644 index 0000000..d2d8240 --- /dev/null +++ b/app/tests/RfpAnalyzer.Tests/Models/ScoringModelsTests.cs @@ -0,0 +1,114 @@ +using RfpAnalyzer.Models; + +namespace RfpAnalyzer.Tests.Models; + +public class ScoringModelsTests +{ + [Fact] + public void ExtractedCriteria_DefaultValues() + { + var criteria = new ExtractedCriteria(); + Assert.Equal("", criteria.RfpTitle); + Assert.Equal(100.0, criteria.TotalWeight); + Assert.Empty(criteria.Criteria); + } + + [Fact] + public void CriterionScore_WeightedScoreCalculation() + { + // This tests the business logic formula: weighted_score = (raw_score * weight) / 100 + var score = new CriterionScore + { + CriterionId = "C-1", + CriterionName = "Technical", + Weight = 30, + RawScore = 85, + WeightedScore = 85 * 30.0 / 100 // = 25.5 + }; + + Assert.Equal(25.5, score.WeightedScore); + } + + [Fact] + public void ProposalEvaluation_DefaultValues() + { + var eval = new ProposalEvaluation(); + Assert.Equal("", eval.RfpTitle); + Assert.Equal(0, eval.TotalScore); + Assert.Equal("", eval.Grade); + Assert.Empty(eval.CriterionScores); + Assert.Empty(eval.OverallStrengths); + Assert.Empty(eval.OverallWeaknesses); + } + + [Theory] + [InlineData(95, "A")] + [InlineData(90, "A")] + [InlineData(85, "B")] + [InlineData(80, "B")] + [InlineData(75, "C")] + [InlineData(70, "C")] + [InlineData(65, "D")] + [InlineData(60, "D")] + [InlineData(55, "F")] + [InlineData(0, "F")] + public void GradeAssignment_MatchesPythonLogic(double score, string expectedGrade) + { + // This replicates the grade assignment from scoring_agent_v2.py + string grade = score switch + { + >= 90 => "A", + >= 80 => "B", + >= 70 => "C", + >= 60 => "D", + _ => "F" + }; + Assert.Equal(expectedGrade, grade); + } + + [Fact] + public void WeightNormalization_SumsTo100() + { + // Test the weight normalization logic from CriteriaExtractionAgent._parse_response() + var criteria = new List + { + new() { CriterionId = "C-1", Weight = 40 }, + new() { CriterionId = "C-2", Weight = 30 }, + new() { CriterionId = "C-3", Weight = 50 } // Total: 120, not 100 + }; + + var totalWeight = criteria.Sum(c => c.Weight); + if (Math.Abs(totalWeight - 100) > 0.1) + { + foreach (var c in criteria) + { + c.Weight = c.Weight / totalWeight * 100; + } + } + + var newTotal = criteria.Sum(c => c.Weight); + Assert.Equal(100.0, newTotal, 1); // within 0.1 + + // Verify proportions maintained + Assert.Equal(100.0 * 40 / 120, criteria[0].Weight, 1); + Assert.Equal(100.0 * 30 / 120, criteria[1].Weight, 1); + Assert.Equal(100.0 * 50 / 120, criteria[2].Weight, 1); + } + + [Fact] + public void TotalScoreCalculation_SumOfWeightedScores() + { + // Test total_score = SUM(weighted_scores) + var scores = new List + { + new() { RawScore = 80, Weight = 30, WeightedScore = 24.0 }, + new() { RawScore = 90, Weight = 25, WeightedScore = 22.5 }, + new() { RawScore = 70, Weight = 20, WeightedScore = 14.0 }, + new() { RawScore = 85, Weight = 15, WeightedScore = 12.75 }, + new() { RawScore = 60, Weight = 10, WeightedScore = 6.0 }, + }; + + var totalScore = Math.Round(scores.Sum(s => s.WeightedScore), 2); + Assert.Equal(79.25, totalScore); + } +} diff --git a/app/tests/RfpAnalyzer.Tests/RfpAnalyzer.Tests.csproj b/app/tests/RfpAnalyzer.Tests/RfpAnalyzer.Tests.csproj new file mode 100644 index 0000000..6bdd1bf --- /dev/null +++ b/app/tests/RfpAnalyzer.Tests/RfpAnalyzer.Tests.csproj @@ -0,0 +1,26 @@ +๏ปฟ + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/tests/RfpAnalyzer.Tests/Services/ComparisonServiceTests.cs b/app/tests/RfpAnalyzer.Tests/Services/ComparisonServiceTests.cs new file mode 100644 index 0000000..f8e53c8 --- /dev/null +++ b/app/tests/RfpAnalyzer.Tests/Services/ComparisonServiceTests.cs @@ -0,0 +1,237 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Moq; +using RfpAnalyzer.Models; +using RfpAnalyzer.Services; + +namespace RfpAnalyzer.Tests.Services; + +public class ComparisonServiceTests +{ + private readonly ComparisonService _service; + + public ComparisonServiceTests() + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AZURE_OPENAI_ENDPOINT"] = "https://test.openai.azure.com/", + ["AZURE_OPENAI_DEPLOYMENT_NAME"] = "gpt-4o-mini" + }) + .Build(); + + var logger = new Mock>(); + _service = new ComparisonService(config, logger.Object); + } + + [Fact] + public void CleanJsonResponse_RemovesJsonCodeFence() + { + var input = "```json\n{\"key\": \"value\"}\n```"; + var result = ComparisonService.CleanJsonResponse(input); + Assert.Equal("{\"key\": \"value\"}", result); + } + + [Fact] + public void CleanJsonResponse_HandlesCleanJson() + { + var input = "{\"key\": \"value\"}"; + var result = ComparisonService.CleanJsonResponse(input); + Assert.Equal("{\"key\": \"value\"}", result); + } + + [Fact] + public void GenerateCsvReport_ProducesValidOutput() + { + var comparison = new ComparisonResult + { + RfpTitle = "Test RFP", + ComparisonDate = "2025-12-15", + TotalVendors = 2, + VendorRankings = new List + { + new() { Rank = 1, VendorName = "Vendor A", TotalScore = 85.0, Grade = "B", Recommendation = "Recommended" }, + new() { Rank = 2, VendorName = "Vendor B", TotalScore = 72.0, Grade = "C", Recommendation = "Consider" } + }, + ComparisonInsights = new List { "Vendor A leads in technical areas" }, + SelectionRecommendation = "Select Vendor A" + }; + + var evaluations = new List + { + new() + { + SupplierName = "Vendor A", + TotalScore = 85.0, + CriterionScores = new List + { + new() { CriterionName = "Technical", Weight = 50, RawScore = 90, WeightedScore = 45.0 }, + new() { CriterionName = "Price", Weight = 50, RawScore = 80, WeightedScore = 40.0 } + } + }, + new() + { + SupplierName = "Vendor B", + TotalScore = 72.0, + CriterionScores = new List + { + new() { CriterionName = "Technical", Weight = 50, RawScore = 70, WeightedScore = 35.0 }, + new() { CriterionName = "Price", Weight = 50, RawScore = 74, WeightedScore = 37.0 } + } + } + }; + + var csvBytes = _service.GenerateCsvReport(comparison, evaluations); + + Assert.NotNull(csvBytes); + Assert.True(csvBytes.Length > 0); + + var csvContent = System.Text.Encoding.UTF8.GetString(csvBytes); + Assert.Contains("Test RFP", csvContent); + Assert.Contains("Vendor A", csvContent); + Assert.Contains("Vendor B", csvContent); + Assert.Contains("VENDOR RANKINGS", csvContent); + Assert.Contains("CRITERION COMPARISON", csvContent); + Assert.Contains("Select Vendor A", csvContent); + } + + [Fact] + public void GenerateExcelReport_ProducesValidOutput() + { + var comparison = new ComparisonResult + { + RfpTitle = "Test RFP", + ComparisonDate = "2025-12-15", + TotalVendors = 1, + VendorRankings = new List + { + new() { Rank = 1, VendorName = "Vendor A", TotalScore = 85.0, Grade = "B", Recommendation = "Select" } + } + }; + + var evaluations = new List + { + new() + { + SupplierName = "Vendor A", + TotalScore = 85.0, + CriterionScores = new List + { + new() { CriterionName = "Quality", Weight = 60, RawScore = 90 }, + new() { CriterionName = "Price", Weight = 40, RawScore = 78 } + } + } + }; + + var excelBytes = _service.GenerateExcelReport(comparison, evaluations); + + Assert.NotNull(excelBytes); + Assert.True(excelBytes.Length > 0); + } + + [Fact] + public void GenerateCsvReport_EmptyEvaluations_ProducesValidOutput() + { + var comparison = new ComparisonResult + { + RfpTitle = "Empty RFP", + ComparisonDate = "2025-12-15", + TotalVendors = 0, + ComparisonInsights = new List(), + SelectionRecommendation = "No vendors to compare" + }; + + var csvBytes = _service.GenerateCsvReport(comparison, new List()); + + Assert.NotNull(csvBytes); + var csvContent = System.Text.Encoding.UTF8.GetString(csvBytes); + Assert.Contains("Empty RFP", csvContent); + Assert.Contains("No vendors to compare", csvContent); + } + + [Fact] + public void GenerateCsvReport_EscapesCsvSpecialCharacters() + { + var comparison = new ComparisonResult + { + RfpTitle = "Test RFP", + ComparisonDate = "2025-12-15", + TotalVendors = 1, + VendorRankings = new List + { + new() + { + Rank = 1, + VendorName = "Vendor A", + TotalScore = 85.0, + Grade = "B", + Recommendation = "Select \"this\" vendor, definitely" + } + }, + ComparisonInsights = new List(), + SelectionRecommendation = "Go with it" + }; + + var csvBytes = _service.GenerateCsvReport(comparison, new List()); + var csvContent = System.Text.Encoding.UTF8.GetString(csvBytes); + + // Recommendation with commas and quotes should be escaped + Assert.Contains("\"Select \"\"this\"\" vendor, definitely\"", csvContent); + } + + [Fact] + public void CreateComparisonAgent_ThrowsWhenEndpointMissing() + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary()) + .Build(); + var logger = new Mock>(); + var service = new ComparisonService(config, logger.Object); + + Assert.Throws(() => service.CreateComparisonAgent()); + } + + [Fact] + public void CreateComparisonAgent_ThrowsWhenDeploymentMissing() + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AZURE_OPENAI_ENDPOINT"] = "https://test.openai.azure.com/" + }) + .Build(); + var logger = new Mock>(); + var service = new ComparisonService(config, logger.Object); + + Assert.Throws(() => service.CreateComparisonAgent()); + } + + [Fact] + public void CreateComparisonAgent_ReturnsAIAgent() + { + var agent = _service.CreateComparisonAgent(); + Assert.NotNull(agent); + } + + [Fact] + public void CreateComparisonOrchestrator_ReturnsAIAgent() + { + var comparisonAgent = _service.CreateComparisonAgent(); + var orchestrator = _service.CreateComparisonOrchestrator(comparisonAgent); + Assert.NotNull(orchestrator); + } + + [Fact] + public void CreateComparisonOrchestrator_ThrowsWhenEndpointMissing() + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary()) + .Build(); + var logger = new Mock>(); + var service = new ComparisonService(config, logger.Object); + + // Create agent with valid config, then try orchestrator with invalid config + var agent = _service.CreateComparisonAgent(); + Assert.Throws(() => service.CreateComparisonOrchestrator(agent)); + } +} diff --git a/app/tests/RfpAnalyzer.Tests/Services/DocumentProcessorServiceTests.cs b/app/tests/RfpAnalyzer.Tests/Services/DocumentProcessorServiceTests.cs new file mode 100644 index 0000000..94131fe --- /dev/null +++ b/app/tests/RfpAnalyzer.Tests/Services/DocumentProcessorServiceTests.cs @@ -0,0 +1,109 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Moq; +using RfpAnalyzer.Models; +using RfpAnalyzer.Services; + +namespace RfpAnalyzer.Tests.Services; + +public class DocumentProcessorServiceTests +{ + private DocumentProcessorService CreateService(Dictionary config) + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(config) + .Build(); + var logger = new Mock>(); + var httpClientFactory = new Mock(); + httpClientFactory.Setup(f => f.CreateClient(It.IsAny())).Returns(new HttpClient()); + return new DocumentProcessorService(httpClientFactory.Object, configuration, logger.Object); + } + + [Fact] + public async Task ExtractContentAsync_PlainText_ReturnsContentDirectly() + { + var service = CreateService(new Dictionary()); + var content = "Hello, World!"u8.ToArray(); + + var result = await service.ExtractContentAsync(content, "test.txt", ExtractionService.ContentUnderstanding); + + Assert.Equal("Hello, World!", result); + } + + [Fact] + public async Task ExtractContentAsync_Markdown_ReturnsContentDirectly() + { + var service = CreateService(new Dictionary()); + var content = "# Heading\n\nSome content"u8.ToArray(); + + var result = await service.ExtractContentAsync(content, "test.md", ExtractionService.DocumentIntelligence); + + Assert.Equal("# Heading\n\nSome content", result); + } + + [Fact] + public async Task ExtractContentAsync_DocumentIntelligence_ThrowsWhenEndpointMissing() + { + var service = CreateService(new Dictionary()); + var content = new byte[] { 1, 2, 3 }; + + await Assert.ThrowsAsync( + () => service.ExtractContentAsync(content, "test.pdf", ExtractionService.DocumentIntelligence)); + } + + [Fact] + public async Task ExtractContentAsync_ContentUnderstanding_ThrowsWhenEndpointMissing() + { + var service = CreateService(new Dictionary()); + var content = new byte[] { 1, 2, 3 }; + + await Assert.ThrowsAsync( + () => service.ExtractContentAsync(content, "test.pdf", ExtractionService.ContentUnderstanding)); + } + + [Fact] + public async Task ExtractContentAsync_InvalidService_ThrowsArgumentOutOfRange() + { + var service = CreateService(new Dictionary()); + var content = new byte[] { 1, 2, 3 }; + + await Assert.ThrowsAsync( + () => service.ExtractContentAsync(content, "test.pdf", (ExtractionService)999)); + } + + [Theory] + [InlineData("test.txt")] + [InlineData("README.md")] + [InlineData("notes.TXT")] + [InlineData("doc.MD")] + public async Task ExtractContentAsync_TextExtensions_SkipsAzureService(string filename) + { + var service = CreateService(new Dictionary()); + var content = "plain text content"u8.ToArray(); + + // Should not throw even without endpoint configured, because text files bypass Azure services + var result = await service.ExtractContentAsync(content, filename, ExtractionService.DocumentIntelligence); + + Assert.Equal("plain text content", result); + } + + [Fact] + public async Task ExtractContentAsync_DocumentIntelligence_FallsBackToContentUnderstandingEndpoint() + { + // When AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT is not set but + // AZURE_CONTENT_UNDERSTANDING_ENDPOINT is, it should use the fallback. + // We can't fully test the SDK call without a real endpoint, but we can verify + // the configuration fallback doesn't throw InvalidOperationException for missing endpoint. + var service = CreateService(new Dictionary + { + ["AZURE_CONTENT_UNDERSTANDING_ENDPOINT"] = "https://test.cognitiveservices.azure.com/" + }); + + // This will fail with an auth/network error (not a config error), proving fallback works + var ex = await Assert.ThrowsAnyAsync( + () => service.ExtractContentAsync(new byte[] { 1, 2, 3 }, "test.pdf", ExtractionService.DocumentIntelligence)); + + // Should NOT be InvalidOperationException about missing config + Assert.IsNotType(ex); + } +} diff --git a/app/tests/RfpAnalyzer.Tests/Services/ScoringServiceTests.cs b/app/tests/RfpAnalyzer.Tests/Services/ScoringServiceTests.cs new file mode 100644 index 0000000..cb668a8 --- /dev/null +++ b/app/tests/RfpAnalyzer.Tests/Services/ScoringServiceTests.cs @@ -0,0 +1,417 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Moq; +using RfpAnalyzer.Models; +using RfpAnalyzer.Services; + +namespace RfpAnalyzer.Tests.Services; + +public class ScoringServiceTests +{ + private readonly ScoringService _service; + + public ScoringServiceTests() + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AZURE_OPENAI_ENDPOINT"] = "https://test.openai.azure.com/", + ["AZURE_OPENAI_DEPLOYMENT_NAME"] = "gpt-4o-mini" + }) + .Build(); + + var logger = new Mock>(); + _service = new ScoringService(config, logger.Object); + } + + [Fact] + public void CleanJsonResponse_RemovesJsonCodeFence() + { + var input = "```json\n{\"key\": \"value\"}\n```"; + var result = ScoringService.CleanJsonResponse(input); + Assert.Equal("{\"key\": \"value\"}", result); + } + + [Fact] + public void CleanJsonResponse_RemovesGenericCodeFence() + { + var input = "```\n{\"key\": \"value\"}\n```"; + var result = ScoringService.CleanJsonResponse(input); + Assert.Equal("{\"key\": \"value\"}", result); + } + + [Fact] + public void CleanJsonResponse_HandlesCleanJson() + { + var input = "{\"key\": \"value\"}"; + var result = ScoringService.CleanJsonResponse(input); + Assert.Equal("{\"key\": \"value\"}", result); + } + + [Fact] + public void CleanJsonResponse_TrimsWhitespace() + { + var input = " \n {\"key\": \"value\"} \n "; + var result = ScoringService.CleanJsonResponse(input); + Assert.Equal("{\"key\": \"value\"}", result); + } + + [Fact] + public void ParseCriteriaResponse_ValidJson_ExtractsCorrectly() + { + var json = """ + { + "rfp_title": "Cloud Migration RFP", + "rfp_summary": "Seeking vendor for enterprise cloud migration", + "total_weight": 100.0, + "criteria": [ + { + "criterion_id": "C-1", + "name": "Technical Capability", + "description": "Cloud migration expertise", + "category": "Technical", + "weight": 40, + "max_score": 100, + "evaluation_guidance": "Assess cloud platform expertise" + }, + { + "criterion_id": "C-2", + "name": "Experience", + "description": "Prior migration projects", + "category": "Experience", + "weight": 30, + "max_score": 100, + "evaluation_guidance": "Evaluate track record" + }, + { + "criterion_id": "C-3", + "name": "Pricing", + "description": "Cost effectiveness", + "category": "Financial", + "weight": 30, + "max_score": 100, + "evaluation_guidance": "Compare value for money" + } + ], + "extraction_notes": "Three key criteria identified" + } + """; + + var result = _service.ParseCriteriaResponse(json); + + Assert.Equal("Cloud Migration RFP", result.RfpTitle); + Assert.Equal("Seeking vendor for enterprise cloud migration", result.RfpSummary); + Assert.Equal(3, result.Criteria.Count); + Assert.Equal(100.0, result.TotalWeight); + Assert.Equal("C-1", result.Criteria[0].CriterionId); + Assert.Equal("Technical Capability", result.Criteria[0].Name); + Assert.Equal(40, result.Criteria[0].Weight); + Assert.Equal("Three key criteria identified", result.ExtractionNotes); + } + + [Fact] + public void ParseCriteriaResponse_WithCodeFence_ParsesCorrectly() + { + var json = "```json\n" + + "{\n" + + " \"rfp_title\": \"Test RFP\",\n" + + " \"rfp_summary\": \"Test summary\",\n" + + " \"criteria\": [\n" + + " { \"criterion_id\": \"C-1\", \"name\": \"Quality\", \"weight\": 50, \"max_score\": 100 },\n" + + " { \"criterion_id\": \"C-2\", \"name\": \"Price\", \"weight\": 50, \"max_score\": 100 }\n" + + " ]\n" + + "}\n" + + "```"; + + var result = _service.ParseCriteriaResponse(json); + + Assert.Equal("Test RFP", result.RfpTitle); + Assert.Equal(2, result.Criteria.Count); + Assert.Equal(100.0, result.Criteria.Sum(c => c.Weight), 1); + } + + [Fact] + public void ParseCriteriaResponse_WeightsNormalized_WhenNotSummingTo100() + { + var json = """ + { + "rfp_title": "Test RFP", + "rfp_summary": "Test", + "criteria": [ + { "criterion_id": "C-1", "name": "A", "weight": 40, "max_score": 100 }, + { "criterion_id": "C-2", "name": "B", "weight": 30, "max_score": 100 }, + { "criterion_id": "C-3", "name": "C", "weight": 50, "max_score": 100 } + ] + } + """; + + var result = _service.ParseCriteriaResponse(json); + + // Original weights sum to 120, should normalize to 100 + Assert.Equal(100.0, result.Criteria.Sum(c => c.Weight), 1); + Assert.Equal(100.0, result.TotalWeight); + } + + [Fact] + public void ParseCriteriaResponse_InvalidJson_ReturnsDefaultCriteria() + { + var json = "this is not valid json at all"; + + var result = _service.ParseCriteriaResponse(json); + + Assert.Equal("Unknown RFP", result.RfpTitle); + Assert.Contains("Error parsing response", result.ExtractionNotes); + Assert.Empty(result.Criteria); + } + + [Fact] + public void ParseScoringResponse_ValidJson_ExtractsCorrectly() + { + var criteria = new ExtractedCriteria + { + RfpTitle = "Test RFP", + RfpSummary = "Test summary" + }; + + var json = """ + { + "rfp_title": "Test RFP", + "supplier_name": "Acme Corp", + "supplier_site": "New York", + "response_id": "RESP-2025-0001", + "evaluation_date": "2025-12-15", + "total_score": 82.5, + "score_percentage": 82.5, + "grade": "B", + "recommendation": "Strong candidate", + "criterion_scores": [ + { + "criterion_id": "C-1", + "criterion_name": "Technical", + "weight": 40, + "raw_score": 85, + "weighted_score": 34.0, + "evidence": "Strong cloud experience", + "justification": "Demonstrated AWS expertise", + "strengths": ["AWS certified", "Large team"], + "gaps": ["Limited Azure experience"] + }, + { + "criterion_id": "C-2", + "criterion_name": "Experience", + "weight": 30, + "raw_score": 90, + "weighted_score": 27.0, + "evidence": "10 years in industry", + "justification": "Extensive portfolio", + "strengths": ["Fortune 500 clients"], + "gaps": [] + }, + { + "criterion_id": "C-3", + "criterion_name": "Pricing", + "weight": 30, + "raw_score": 75, + "weighted_score": 22.5, + "evidence": "Competitive pricing", + "justification": "Good value", + "strengths": ["Flexible pricing"], + "gaps": ["No volume discounts"] + } + ], + "executive_summary": "Acme Corp is a strong candidate.", + "overall_strengths": ["Technical expertise", "Industry experience"], + "overall_weaknesses": ["Limited Azure experience"], + "recommendations": ["Consider for shortlist"], + "risk_assessment": "Low risk overall" + } + """; + + var result = _service.ParseScoringResponse(json, criteria); + + Assert.Equal("Acme Corp", result.SupplierName); + Assert.Equal("New York", result.SupplierSite); + Assert.Equal(3, result.CriterionScores.Count); + // Total = 34.0 + 27.0 + 22.5 = 83.5 + Assert.Equal(83.5, result.TotalScore); + Assert.Equal("B", result.Grade); + Assert.Equal(2, result.OverallStrengths.Count); + Assert.Single(result.OverallWeaknesses); + Assert.Equal("Low risk overall", result.RiskAssessment); + } + + [Fact] + public void ParseScoringResponse_InvalidJson_ReturnsDefaultEvaluation() + { + var criteria = new ExtractedCriteria + { + RfpTitle = "Test RFP", + RfpSummary = "Test summary" + }; + + var result = _service.ParseScoringResponse("not json", criteria); + + Assert.Equal("Test RFP", result.RfpTitle); + Assert.Equal("Unknown Vendor", result.SupplierName); + Assert.Equal("F", result.Grade); + } + + [Fact] + public void ParseScoringResponse_RecalculatesGradeFromWeightedScores() + { + var criteria = new ExtractedCriteria { RfpTitle = "Test" }; + + var json = """ + { + "supplier_name": "Vendor A", + "grade": "A", + "criterion_scores": [ + { "criterion_id": "C-1", "weight": 50, "raw_score": 60, "weighted_score": 30.0 }, + { "criterion_id": "C-2", "weight": 50, "raw_score": 50, "weighted_score": 25.0 } + ] + } + """; + + var result = _service.ParseScoringResponse(json, criteria); + + // Total = 30.0 + 25.0 = 55.0 -> Grade F (not A as stated in JSON) + Assert.Equal(55.0, result.TotalScore); + Assert.Equal("F", result.Grade); // Recalculated, not blindly trusted + } + + [Fact] + public void ParseScoringResponse_MissingOptionalFields_UsesDefaults() + { + var criteria = new ExtractedCriteria { RfpTitle = "Fallback Title" }; + + var json = """ + { + "criterion_scores": [ + { "criterion_id": "C-1", "weighted_score": 45.0 } + ] + } + """; + + var result = _service.ParseScoringResponse(json, criteria); + + Assert.Equal("Fallback Title", result.RfpTitle); + Assert.Equal("Unknown Vendor", result.SupplierName); + Assert.Single(result.CriterionScores); + } + + [Theory] + [InlineData(95.0, "A")] + [InlineData(90.0, "A")] + [InlineData(85.0, "B")] + [InlineData(80.0, "B")] + [InlineData(75.0, "C")] + [InlineData(70.0, "C")] + [InlineData(65.0, "D")] + [InlineData(60.0, "D")] + [InlineData(55.0, "F")] + [InlineData(0.0, "F")] + public void ParseScoringResponse_GradeAssignment_MatchesThresholds(double score, string expectedGrade) + { + var criteria = new ExtractedCriteria { RfpTitle = "Test" }; + var json = $$""" + { + "supplier_name": "Test Vendor", + "criterion_scores": [ + { "criterion_id": "C-1", "weighted_score": {{score}} } + ] + } + """; + + var result = _service.ParseScoringResponse(json, criteria); + Assert.Equal(expectedGrade, result.Grade); + } + + [Fact] + public void CreateCriteriaExtractionAgent_ThrowsWhenEndpointMissing() + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary()) + .Build(); + var logger = new Mock>(); + var service = new ScoringService(config, logger.Object); + + Assert.Throws(() => service.CreateCriteriaExtractionAgent()); + } + + [Fact] + public void CreateProposalScoringAgent_ThrowsWhenEndpointMissing() + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary()) + .Build(); + var logger = new Mock>(); + var service = new ScoringService(config, logger.Object); + + Assert.Throws(() => service.CreateProposalScoringAgent("[]")); + } + + [Fact] + public void CreateOrchestratorAgent_ThrowsWhenEndpointMissing() + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary()) + .Build(); + var logger = new Mock>(); + var service = new ScoringService(config, logger.Object); + + // Need valid agents to pass to orchestrator - but creating them also requires config, + // so test orchestrator creation with valid config but verify it composes agents + var configValid = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AZURE_OPENAI_ENDPOINT"] = "https://test.openai.azure.com/", + ["AZURE_OPENAI_DEPLOYMENT_NAME"] = "gpt-4o-mini" + }) + .Build(); + var serviceValid = new ScoringService(configValid, logger.Object); + var criteriaAgent = serviceValid.CreateCriteriaExtractionAgent(); + var scoringAgent = serviceValid.CreateProposalScoringAgent("[]"); + + // Orchestrator requires its own endpoint to create the ChatClient + Assert.Throws(() => service.CreateOrchestratorAgent(criteriaAgent, scoringAgent)); + } + + [Fact] + public void CreateOrchestratorAgent_CreatesWithValidConfig() + { + // The orchestrator agent should compose the specialist agents as function tools + var criteriaAgent = _service.CreateCriteriaExtractionAgent(); + var scoringAgent = _service.CreateProposalScoringAgent("[]"); + var orchestrator = _service.CreateOrchestratorAgent(criteriaAgent, scoringAgent); + + Assert.NotNull(orchestrator); + } + + [Fact] + public void CreateCriteriaExtractionAgent_ReturnsAIAgent() + { + var agent = _service.CreateCriteriaExtractionAgent(); + Assert.NotNull(agent); + } + + [Fact] + public void CreateProposalScoringAgent_ReturnsAIAgent() + { + var agent = _service.CreateProposalScoringAgent("[]"); + Assert.NotNull(agent); + } + + [Fact] + public void EvaluationMetadata_UsesAgentFrameworkType() + { + // Verify metadata reflects the Microsoft Agent Framework usage + var metadata = new EvaluationMetadata + { + Version = "3.0", + EvaluationType = "microsoft-agent-framework" + }; + + Assert.Equal("3.0", metadata.Version); + Assert.Equal("microsoft-agent-framework", metadata.EvaluationType); + } +} diff --git a/app/uv.lock b/app/uv.lock deleted file mode 100644 index a7639ee..0000000 --- a/app/uv.lock +++ /dev/null @@ -1,3619 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.13" -resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version < '3.14'", -] - -[options] -prerelease-mode = "allow" - -[[package]] -name = "a2a-sdk" -version = "0.3.22" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "protobuf" }, - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/92/a3/76f2d94a32a1b0dc760432d893a09ec5ed31de5ad51b1ef0f9d199ceb260/a2a_sdk-0.3.22.tar.gz", hash = "sha256:77a5694bfc4f26679c11b70c7f1062522206d430b34bc1215cfbb1eba67b7e7d", size = 231535, upload-time = "2025-12-16T18:39:21.19Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/e8/f4e39fd1cf0b3c4537b974637143f3ebfe1158dad7232d9eef15666a81ba/a2a_sdk-0.3.22-py3-none-any.whl", hash = "sha256:b98701135bb90b0ff85d35f31533b6b7a299bf810658c1c65f3814a6c15ea385", size = 144347, upload-time = "2025-12-16T18:39:19.218Z" }, -] - -[[package]] -name = "ag-ui-protocol" -version = "0.1.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/67/bb/5a5ec893eea5805fb9a3db76a9888c3429710dfb6f24bbb37568f2cf7320/ag_ui_protocol-0.1.10.tar.gz", hash = "sha256:3213991c6b2eb24bb1a8c362ee270c16705a07a4c5962267a083d0959ed894f4", size = 6945, upload-time = "2025-11-06T15:17:17.068Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/78/eb55fabaab41abc53f52c0918a9a8c0f747807e5306273f51120fd695957/ag_ui_protocol-0.1.10-py3-none-any.whl", hash = "sha256:c81e6981f30aabdf97a7ee312bfd4df0cd38e718d9fc10019c7d438128b93ab5", size = 7889, upload-time = "2025-11-06T15:17:15.325Z" }, -] - -[[package]] -name = "agent-framework" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core", extra = ["all"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7e/e7/5ad52075da4e586ca94fb8806b3085ac5dea8059413e413bff88c0452e88/agent_framework-1.0.0b260107.tar.gz", hash = "sha256:a2f6508a0ca1df3b7ca4e3a64e45bac8e33cdfe02cf69e9056e37e881a58aad7", size = 2898189, upload-time = "2026-01-07T23:57:48.213Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/55/ffef27526cc26bf163ccf9d58ba87bf4e677bba343a542e7b666846f744d/agent_framework-1.0.0b260107-py3-none-any.whl", hash = "sha256:080deb32bff4ef07227a4ba709798c67079ff8a2997fe7a0aed0010adc0c18cf", size = 5554, upload-time = "2026-01-07T23:57:08.433Z" }, -] - -[[package]] -name = "agent-framework-a2a" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "a2a-sdk" }, - { name = "agent-framework-core" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/9a/7314f4b4b9b3dffb0ace8681baf0e330a7fd8de55deb09f917024b854b3d/agent_framework_a2a-1.0.0b260107.tar.gz", hash = "sha256:f22f4eff856dd93d32ec07ffc30608ca54308c4fdcc007c028d8616314893b46", size = 7281, upload-time = "2026-01-07T23:57:05.228Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/92/45e37c57427b9613e54fc3ad865cfc4d4784a0287576b55fd6f3f884056b/agent_framework_a2a-1.0.0b260107-py3-none-any.whl", hash = "sha256:e56f9836c6fb5d60b0750a8a1339f0f09cec6e3ea2ef3bf327ea5c10378b7dff", size = 7502, upload-time = "2026-01-07T23:57:41.519Z" }, -] - -[[package]] -name = "agent-framework-ag-ui" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ag-ui-protocol" }, - { name = "agent-framework-core" }, - { name = "fastapi" }, - { name = "uvicorn" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a2/d5/11fe7cae81192d0ffe816c59ddf0284b947a7a32da3072c99f2bb11e9a5c/agent_framework_ag_ui-1.0.0b260107.tar.gz", hash = "sha256:c0f79f08c3ea2c1a6454fab8cd46a5f94df2e8db71a76b5d7906735087f66349", size = 85637, upload-time = "2026-01-07T23:57:20.325Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/5b/3675630c6ed72213c2309c1b6b92a7b9496e42ca249826625c8cb4e16796/agent_framework_ag_ui-1.0.0b260107-py3-none-any.whl", hash = "sha256:532a34ebbb761cf5511db4ac6b1c5461cf0ee266bf0ccd961f4f8fb9ca5dff5f", size = 62472, upload-time = "2026-01-07T23:57:44.624Z" }, -] - -[[package]] -name = "agent-framework-anthropic" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "anthropic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1a/d4/9d002f6333f45d453fc8766b73df0d9fb69e486c678abea017215949e66d/agent_framework_anthropic-1.0.0b260107.tar.gz", hash = "sha256:731d8d16e4a39030e382ae826f0fd123b04a64c4020435ad0ba6290bd461b2f3", size = 9321, upload-time = "2026-01-07T23:57:42.567Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/75/daaabe378802a918d7bceb6c52e04b332112c89c819f9eaaa00f1f1f37b0/agent_framework_anthropic-1.0.0b260107-py3-none-any.whl", hash = "sha256:47a4fe893769a15594c663ae2f27132f32cea4393bffe4578a1df49ee70f8a23", size = 9322, upload-time = "2026-01-07T23:57:34.081Z" }, -] - -[[package]] -name = "agent-framework-azure-ai" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "aiohttp" }, - { name = "azure-ai-agents" }, - { name = "azure-ai-projects" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/26/954d48dbe6e2d558e8425dce1a62238787350f501443081f0de9eab0d9c5/agent_framework_azure_ai-1.0.0b260107.tar.gz", hash = "sha256:bfbec64bf89382833aea18526bb4970b540f9afb269a0eb96bbaed07a3ae6f66", size = 19840, upload-time = "2026-01-07T23:57:09.976Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/c0/ca16e4d772baa2b9d94efebefb5c0d795cddc1428b25a40f4eee7eec8415/agent_framework_azure_ai-1.0.0b260107-py3-none-any.whl", hash = "sha256:001f82bec04d73a8d5e0cf34a9f613963e50db7d46ae000625554306c8271976", size = 21431, upload-time = "2026-01-07T23:57:39.497Z" }, -] - -[[package]] -name = "agent-framework-azure-ai-search" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "azure-search-documents" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/e6/15f6bb752e900a4262bc2469c3947d7bd85793ebe88b596fa7ea11c0eec5/agent_framework_azure_ai_search-1.0.0b260107.tar.gz", hash = "sha256:1037e1addcab8805f000b0a24725470715fcd758b2a165650a28583dcd30d1b1", size = 13317, upload-time = "2026-01-07T23:57:21.682Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/c9/81379dca1f280222170d6561d63f5ed1f0e2477e51926f081d4e7cd2bb88/agent_framework_azure_ai_search-1.0.0b260107-py3-none-any.whl", hash = "sha256:59dd3e559ca2920b952c4786b4889e060fa7b0f4df1e236c43a82e92142aaa86", size = 13447, upload-time = "2026-01-07T23:57:18.192Z" }, -] - -[[package]] -name = "agent-framework-azurefunctions" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "azure-functions" }, - { name = "azure-functions-durable" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/74/94a8e1aa0f4264f75c992d76f61fc13f73ba28ecfaabebb132b76a77aa9c/agent_framework_azurefunctions-1.0.0b260107.tar.gz", hash = "sha256:83c22ecd1706593e5223cafd0c348a4cf2d3379d8d06528940e2d77cb66c752e", size = 33705, upload-time = "2026-01-07T23:57:28.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/b7/e0ac2145d7c7dadca7c7cae03d31f097e9b913c132311fc5e781efe351a4/agent_framework_azurefunctions-1.0.0b260107-py3-none-any.whl", hash = "sha256:97581152a4d4e7a9dad1199e5d748bb77ef63522572d5c6cb9de4717372b2037", size = 37356, upload-time = "2026-01-07T23:57:11.156Z" }, -] - -[[package]] -name = "agent-framework-chatkit" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "openai-chatkit" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fb/8a/c0d1afda3707f9a369be8a235a493ce6c3a645fe87b9ce414dbac97373cd/agent_framework_chatkit-1.0.0b260107.tar.gz", hash = "sha256:9bd46fe9f22acb741c75bde038d738489a518c30dad56b16ad26592598e870f5", size = 12428, upload-time = "2026-01-07T23:57:40.476Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/cd/d7e578239a89977028584dfc8494901cb83824a0f1045369ed55f1dd9c7d/agent_framework_chatkit-1.0.0b260107-py3-none-any.whl", hash = "sha256:88665fd24bafb78b8649d10d267dd27f62cac0b70489044299574288ba8457f3", size = 11726, upload-time = "2026-01-07T23:57:04.074Z" }, -] - -[[package]] -name = "agent-framework-copilotstudio" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "microsoft-agents-copilotstudio-client" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/41/e7/43d3f8b4650b4c4ff214a6340676b7d3bd8087ba280fbbfedc91746bcabf/agent_framework_copilotstudio-1.0.0b260107.tar.gz", hash = "sha256:72d53bd625540786c0989c78e3f57a5941349ec2dc0dfc74c4bd85e0c4e79b47", size = 8525, upload-time = "2026-01-07T23:57:46.483Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/3c/2fbe13fbcc97a4568d34604f1a730af2699b201c50627a918aa02951a680/agent_framework_copilotstudio-1.0.0b260107-py3-none-any.whl", hash = "sha256:dbd5bf97460de6f40cac524f52acd458cb1a1c6c1cac1c8bb3317edf0112fd90", size = 8711, upload-time = "2026-01-07T23:57:23.31Z" }, -] - -[[package]] -name = "agent-framework-core" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-identity" }, - { name = "mcp", extra = ["ws"] }, - { name = "openai" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, - { name = "opentelemetry-semantic-conventions-ai" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/44/06f5d2c99dd7bdb82c2cb5cbc354b5bc6af72d1886d20eff1dff83508fae/agent_framework_core-1.0.0b260107.tar.gz", hash = "sha256:12636fb64664c6153546f0d85dafccdbe57226767c14b3f38985867389f980bb", size = 3574757, upload-time = "2026-01-07T23:57:16.113Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5a/8c6315a2ca119ad48340344616d4b8e77fd68e2892f82c402069a52ad647/agent_framework_core-1.0.0b260107-py3-none-any.whl", hash = "sha256:5bd119b8d30dc2d5bee1c4a5c3597d7afc808a52e4de148725c4f2d9bcc7632b", size = 5687298, upload-time = "2026-01-07T23:57:26.286Z" }, -] - -[package.optional-dependencies] -all = [ - { name = "agent-framework-a2a" }, - { name = "agent-framework-ag-ui" }, - { name = "agent-framework-anthropic" }, - { name = "agent-framework-azure-ai" }, - { name = "agent-framework-azure-ai-search" }, - { name = "agent-framework-azurefunctions" }, - { name = "agent-framework-chatkit" }, - { name = "agent-framework-copilotstudio" }, - { name = "agent-framework-declarative" }, - { name = "agent-framework-devui" }, - { name = "agent-framework-lab" }, - { name = "agent-framework-mem0" }, - { name = "agent-framework-ollama" }, - { name = "agent-framework-purview" }, - { name = "agent-framework-redis" }, -] - -[[package]] -name = "agent-framework-declarative" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "powerfx", marker = "python_full_version < '3.14'" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/30/22fb13d4ae2a13a138ad245fcfbe9aa38f5b7dbdc0cd9672fd6db874ee92/agent_framework_declarative-1.0.0b260107.tar.gz", hash = "sha256:8edf62c8cae0c67e4cbdb713c0e35c4ceaf7ccabb6f1a2b950d4b8796e29bc84", size = 12757, upload-time = "2026-01-07T23:57:06.318Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/0c/4db67ac51cfad217f1928e3f64ab512ca34e2a7b8d0dfe9e09c6fadecf80/agent_framework_declarative-1.0.0b260107-py3-none-any.whl", hash = "sha256:35004053cbfd0217cf802467d87f51324822be351dd67f5e12f9b851019bb5b0", size = 13510, upload-time = "2026-01-07T23:57:45.632Z" }, -] - -[[package]] -name = "agent-framework-devui" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "fastapi" }, - { name = "python-dotenv" }, - { name = "uvicorn", extra = ["standard"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/cc/144aec868a2d599e5a779a3f17fb98c77ea4e9e8bd909c559981bc789252/agent_framework_devui-1.0.0b260107.tar.gz", hash = "sha256:af025563bd5e7ec626027610fb43553e33a741487465bc9abbcdf11f751860bb", size = 356007, upload-time = "2026-01-07T23:57:13.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/b9/c7b6c12b4e0bfd6f4d4671512cd5805477abf9d1d93201786d24d969bcf2/agent_framework_devui-1.0.0b260107-py3-none-any.whl", hash = "sha256:94039e7a0a0cddf343ee40fd3209bb16b9343c33fcbe288a1b31da19cd991260", size = 361044, upload-time = "2026-01-07T23:57:35.31Z" }, -] - -[[package]] -name = "agent-framework-lab" -version = "1.0.0b251024" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/05/c5/be86273cb3545651d0c8112ff9f38ae8fe13b740ce9b65b9be83ff2d70ee/agent_framework_lab-1.0.0b251024.tar.gz", hash = "sha256:4261cb595b6edfd4f30db613c1885c71b3dcfa2088cf29224d4f17b3ff956b2a", size = 23397, upload-time = "2025-10-24T18:13:48.58Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/0f/3974b2b1f6bf523ee3ced0886b6afd5ca8bbebd24aa5278ef77db0d3d765/agent_framework_lab-1.0.0b251024-py3-none-any.whl", hash = "sha256:1596408991a92fcacef4bb939305d2b59159517b707f48114105fc0dd46bfee7", size = 26589, upload-time = "2025-10-24T18:13:47.229Z" }, -] - -[[package]] -name = "agent-framework-mem0" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "mem0ai" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/49/8c000c562a0bfc2cdf160253a030fbc21771db69c009f9970902e1ddd65b/agent_framework_mem0-1.0.0b260107.tar.gz", hash = "sha256:11c9672e2cd7f2f74213472fd4abed26a913fa6443f9224804f3c9b1b58f74b7", size = 5400, upload-time = "2026-01-07T23:57:36.498Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/07/fae73c5b0045dc78c685348371402a6dbadd83147da744a44ade7d7ad06d/agent_framework_mem0-1.0.0b260107-py3-none-any.whl", hash = "sha256:c52751565da07524bf2317fdd75068bdd03c73b7002d82acee393821485909e6", size = 5573, upload-time = "2026-01-07T23:57:37.421Z" }, -] - -[[package]] -name = "agent-framework-ollama" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "ollama" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/ba/23eaba3ea5220f1752d8d4a398a41951c7f7b1fc650cf1fed48c7e4e5127/agent_framework_ollama-1.0.0b260107.tar.gz", hash = "sha256:412c098eedb170d76e15eadc5b0bc9f5792a7e13d655cb1e7f03e8e9fb4d6950", size = 5982, upload-time = "2026-01-07T23:57:29.759Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/30/f821646487fb08018c240ca1ecbb5c4684378dfb48c192b6c1bf778dc286/agent_framework_ollama-1.0.0b260107-py3-none-any.whl", hash = "sha256:11c46a8495f58a71044c648476ff982fede1ad1e64cda28c9a9128ca3674d7b0", size = 7029, upload-time = "2026-01-07T23:57:07.481Z" }, -] - -[[package]] -name = "agent-framework-purview" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "azure-core" }, - { name = "httpx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/31/e7/097789fad41cdc4c477a78278a25e9af0e35c328dee612ad46bdbdda3e15/agent_framework_purview-1.0.0b260107.tar.gz", hash = "sha256:f12fb52b1d4ce0dc593458182ac901dafaf1bdcca9a86aa7cfe16f27546bcf89", size = 26814, upload-time = "2026-01-07T23:57:30.431Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/43/04a107ae1d46a53c4f9423a87e75c352d4810665d6a8c0b2d28f06f92360/agent_framework_purview-1.0.0b260107-py3-none-any.whl", hash = "sha256:74d39279a84333a7e343fec2e2b4723700b58e2bdb3d18a315af3a03efd77018", size = 26176, upload-time = "2026-01-07T23:57:31.707Z" }, -] - -[[package]] -name = "agent-framework-redis" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "numpy" }, - { name = "redis" }, - { name = "redisvl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ce/9c/57332b52089240adba1fae311893bc003238434ddb31773e82d32a64b4b1/agent_framework_redis-1.0.0b260107.tar.gz", hash = "sha256:a66fb64646521967995ee0ea0970695c66d016838f3f8f965e0c21a406f48c41", size = 15714, upload-time = "2026-01-07T23:57:32.932Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/6e/1aa99fc437481370f5256c23a29ff9899dd6e727af8b928fb06620b339a6/agent_framework_redis-1.0.0b260107-py3-none-any.whl", hash = "sha256:77a4276ece6c28ed65a53a1b399132fe2920f8da9bbd83eb87efb1eb41c44118", size = 16051, upload-time = "2026-01-07T23:57:38.579Z" }, -] - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, -] - -[[package]] -name = "aiohttp" -version = "3.13.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohappyeyeballs" }, - { name = "aiosignal" }, - { name = "attrs" }, - { name = "frozenlist" }, - { name = "multidict" }, - { name = "propcache" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, - { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, - { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, - { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, - { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, - { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, - { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, - { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, - { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, - { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, - { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, - { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, - { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, - { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, - { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, - { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, - { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, - { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, - { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, - { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, - { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, - { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, - { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, - { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, - { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, - { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, - { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, - { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, - { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, -] - -[[package]] -name = "aiosignal" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "frozenlist" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, -] - -[[package]] -name = "altair" -version = "6.1.0.dev20260112" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "jsonschema" }, - { name = "narwhals" }, - { name = "packaging" }, - { name = "typing-extensions", marker = "python_full_version < '3.15'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/a9/d327f56a073000fedd72a82fe5319b6a9ee4951ddaba59175fc1fdac29ed/altair-6.1.0.dev20260112.tar.gz", hash = "sha256:15dfdb1c5d47375a515d938b65acb089220f98c62d70f6614ef8f46cc2726b64", size = 764145, upload-time = "2026-01-12T03:34:16.67Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/1a/11599d88e750bca12fbd6a9063589868e069b16dc0a13d40ce37cd0f8abb/altair-6.1.0.dev20260112-py3-none-any.whl", hash = "sha256:f8defd100c880b0934c53ca09390fcd04d4fdc0aa91ef76911c35f85dafd0fb9", size = 795694, upload-time = "2026-01-12T03:34:15.135Z" }, -] - -[[package]] -name = "annotated-doc" -version = "0.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, -] - -[[package]] -name = "anthropic" -version = "0.75.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "docstring-parser" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/1f/08e95f4b7e2d35205ae5dcbb4ae97e7d477fc521c275c02609e2931ece2d/anthropic-0.75.0.tar.gz", hash = "sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb", size = 439565, upload-time = "2025-11-24T20:41:45.28Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/1c/1cd02b7ae64302a6e06724bf80a96401d5313708651d277b1458504a1730/anthropic-0.75.0-py3-none-any.whl", hash = "sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b", size = 388164, upload-time = "2025-11-24T20:41:43.587Z" }, -] - -[[package]] -name = "anyio" -version = "4.12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, -] - -[[package]] -name = "asgiref" -version = "3.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" }, -] - -[[package]] -name = "attrs" -version = "25.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, -] - -[[package]] -name = "azure-ai-agents" -version = "1.2.0b5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "isodate" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ed/57/8adeed578fa8984856c67b4229e93a58e3f6024417d448d0037aafa4ee9b/azure_ai_agents-1.2.0b5.tar.gz", hash = "sha256:1a16ef3f305898aac552269f01536c34a00473dedee0bca731a21fdb739ff9d5", size = 394876, upload-time = "2025-09-30T01:55:02.328Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/6d/15070d23d7a94833a210da09d5d7ed3c24838bb84f0463895e5d159f1695/azure_ai_agents-1.2.0b5-py3-none-any.whl", hash = "sha256:257d0d24a6bf13eed4819cfa5c12fb222e5908deafb3cbfd5711d3a511cc4e88", size = 217948, upload-time = "2025-09-30T01:55:04.155Z" }, -] - -[[package]] -name = "azure-ai-documentintelligence" -version = "1.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "isodate" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/44/7b/8115cd713e2caa5e44def85f2b7ebd02a74ae74d7113ba20bdd41fd6dd80/azure_ai_documentintelligence-1.0.2.tar.gz", hash = "sha256:4d75a2513f2839365ebabc0e0e1772f5601b3a8c9a71e75da12440da13b63484", size = 170940, upload-time = "2025-03-27T02:46:20.606Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/75/c9ec040f23082f54ffb1977ff8f364c2d21c79a640a13d1c1809e7fd6b1a/azure_ai_documentintelligence-1.0.2-py3-none-any.whl", hash = "sha256:e1fb446abbdeccc9759d897898a0fe13141ed29f9ad11fc705f951925822ed59", size = 106005, upload-time = "2025-03-27T02:46:22.356Z" }, -] - -[[package]] -name = "azure-ai-projects" -version = "2.0.0b3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "azure-identity" }, - { name = "azure-storage-blob" }, - { name = "isodate" }, - { name = "openai" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/24/e0/3512d3f07e9dd2eb4af684387c31598c435bd87833b6a81850972963cb9c/azure_ai_projects-2.0.0b3.tar.gz", hash = "sha256:6d09ad110086e450a47b991ee8a3644f1be97fa3085d5981d543f900d78f4505", size = 431749, upload-time = "2026-01-06T05:31:25.849Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/b6/8fbd4786bb5c0dd19eaff86ddce0fbfb53a6f90d712038272161067a076a/azure_ai_projects-2.0.0b3-py3-none-any.whl", hash = "sha256:3b3048a3ba3904d556ba392b7bd20b6e84c93bb39df6d43a6470cdb0ad08af8c", size = 240717, upload-time = "2026-01-06T05:31:27.716Z" }, -] - -[[package]] -name = "azure-common" -version = "1.1.28" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3e/71/f6f71a276e2e69264a97ad39ef850dca0a04fce67b12570730cb38d0ccac/azure-common-1.1.28.zip", hash = "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3", size = 20914, upload-time = "2022-02-03T19:39:44.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/55/7f118b9c1b23ec15ca05d15a578d8207aa1706bc6f7c87218efffbbf875d/azure_common-1.1.28-py2.py3-none-any.whl", hash = "sha256:5c12d3dcf4ec20599ca6b0d3e09e86e146353d443e7fcc050c9a19c1f9df20ad", size = 14462, upload-time = "2022-02-03T19:39:42.417Z" }, -] - -[[package]] -name = "azure-core" -version = "1.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ef/83/41c9371c8298999c67b007e308a0a3c4d6a59c6908fa9c62101f031f886f/azure_core-1.37.0.tar.gz", hash = "sha256:7064f2c11e4b97f340e8e8c6d923b822978be3016e46b7bc4aa4b337cfb48aee", size = 357620, upload-time = "2025-12-11T20:05:13.518Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/34/a9914e676971a13d6cc671b1ed172f9804b50a3a80a143ff196e52f4c7ee/azure_core-1.37.0-py3-none-any.whl", hash = "sha256:b3abe2c59e7d6bb18b38c275a5029ff80f98990e7c90a5e646249a56630fcc19", size = 214006, upload-time = "2025-12-11T20:05:14.96Z" }, -] - -[[package]] -name = "azure-core-tracing-opentelemetry" -version = "1.0.0b12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "opentelemetry-api" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/7f/5de13a331a5f2919417819cc37dcf7c897018f02f83aa82b733e6629a6a6/azure_core_tracing_opentelemetry-1.0.0b12.tar.gz", hash = "sha256:bb454142440bae11fd9d68c7c1d67ae38a1756ce808c5e4d736730a7b4b04144", size = 26010, upload-time = "2025-03-21T00:18:37.346Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/5e/97a471f66935e7f89f521d0e11ae49c7f0871ca38f5c319dccae2155c8d8/azure_core_tracing_opentelemetry-1.0.0b12-py3-none-any.whl", hash = "sha256:38fd42709f1cc4bbc4f2797008b1c30a6a01617e49910c05daa3a0d0c65053ac", size = 11962, upload-time = "2025-03-21T00:18:38.581Z" }, -] - -[[package]] -name = "azure-functions" -version = "1.25.0b3.dev1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/a3/8d6d1f3d7869363028a2488e6b3fed7375be0c652933a6b701dbe8ebff36/azure_functions-1.25.0b3.dev1.tar.gz", hash = "sha256:f9777661b0fd14e6a6ad7a85bb179ba59c80ffa64ec15f1728848154c9135c2e", size = 142121, upload-time = "2025-12-08T22:00:52.436Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/3f/d3a446d76159cb1e2015e7a24b888d2affc28d68c59795252133e6474cad/azure_functions-1.25.0b3.dev1-py3-none-any.whl", hash = "sha256:3ba27c26310c112d0955e1dae19fa378b40b509ff1c59e1a45826a28042d21a3", size = 114184, upload-time = "2025-12-08T22:00:51.02Z" }, -] - -[[package]] -name = "azure-functions-durable" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "azure-functions" }, - { name = "furl" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, - { name = "python-dateutil" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/51/3a/f168b434fa69eaaf5d14b54d88239b851eceb7e10f666b55289dd0933ccb/azure-functions-durable-1.4.0.tar.gz", hash = "sha256:945488ef28917dae4295a4dd6e6f6601ffabe32e3fbb94ceb261c9b65b6e6c0f", size = 176584, upload-time = "2025-09-24T23:57:46.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/01/7f03229fa5c05a5cc7e41172aef80c5242d28aeea0825f592f93141a4b91/azure_functions_durable-1.4.0-py3-none-any.whl", hash = "sha256:0efe919cdda96924791feabe192a37c7d872414b4c6ce348417a02ee53d8cc31", size = 143159, upload-time = "2025-09-24T23:57:45.294Z" }, -] - -[[package]] -name = "azure-identity" -version = "1.26.0b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "cryptography" }, - { name = "msal" }, - { name = "msal-extensions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d7/b0/0c93d0d35694d5015f565a70ef5428ba640a3ba3bc082e24be4d72a3a915/azure_identity-1.26.0b1.tar.gz", hash = "sha256:401197087ec14ee29cfbfcd099453d56037bef252954fee04b5d26ccb702c869", size = 292298, upload-time = "2025-11-07T03:04:14.504Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/28/af9ef022f21e3b51b3718d4348f771b490678c1116563895547c0a771362/azure_identity-1.26.0b1-py3-none-any.whl", hash = "sha256:dc608b59ae628a38611208ee761adeb1a2b9390258b58d6edcda2d24c50a4348", size = 197227, upload-time = "2025-11-07T03:04:16.923Z" }, -] - -[[package]] -name = "azure-monitor-opentelemetry" -version = "1.8.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "azure-core-tracing-opentelemetry" }, - { name = "azure-monitor-opentelemetry-exporter" }, - { name = "opentelemetry-instrumentation-django" }, - { name = "opentelemetry-instrumentation-fastapi" }, - { name = "opentelemetry-instrumentation-flask" }, - { name = "opentelemetry-instrumentation-psycopg2" }, - { name = "opentelemetry-instrumentation-requests" }, - { name = "opentelemetry-instrumentation-urllib" }, - { name = "opentelemetry-instrumentation-urllib3" }, - { name = "opentelemetry-resource-detector-azure" }, - { name = "opentelemetry-sdk" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8d/a9/f335c32e76e3bac3fbbd7977980f62a7deec5191e984517bdbb38539dfd1/azure_monitor_opentelemetry-1.8.3.tar.gz", hash = "sha256:4aa10f6712db653f618e14e3701de7a2f96669a8f2fea6fb22125077da4ea91c", size = 55177, upload-time = "2025-12-05T00:16:41.491Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/4f/138e2f1eddce9b8dda1cccccb5eb3819c1d4d5ea843fbf09ecc3d810641b/azure_monitor_opentelemetry-1.8.3-py3-none-any.whl", hash = "sha256:647248328bb03f8044918411d57c661230277958559f067892bd79f98ce8f69c", size = 27687, upload-time = "2025-12-05T00:16:42.819Z" }, -] - -[[package]] -name = "azure-monitor-opentelemetry-exporter" -version = "1.0.0b46" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "azure-identity" }, - { name = "msrest" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, - { name = "psutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/57/4dd223fcded4955f85ecfae721802cf4bc5a9a95efd8a9a00271f80d4e6b/azure_monitor_opentelemetry_exporter-1.0.0b46.tar.gz", hash = "sha256:a2fd5837c41b5b10316b089ccbe694fc8a69c23db92a5555b298b1eec3eb38bd", size = 277957, upload-time = "2025-12-04T21:22:41.654Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/f9/5b1273d134743b59a271000c08ea3b686f184bfbd73e5ab3a7feae2d0e5f/azure_monitor_opentelemetry_exporter-1.0.0b46-py2.py3-none-any.whl", hash = "sha256:12935e72dcad4a162636eaa5f861e106fcdc3c19928e79cd58b52653fe15625a", size = 200542, upload-time = "2025-12-04T21:22:43.003Z" }, -] - -[[package]] -name = "azure-search-documents" -version = "11.7.0b2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-common" }, - { name = "azure-core" }, - { name = "isodate" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f9/ba/bde0f03e0a742ba3bbcc929f91ed2f3b1420c2bb84c9a7f878f3b87ebfce/azure_search_documents-11.7.0b2.tar.gz", hash = "sha256:b6e039f8038ff2210d2057e704e867c6e29bb46bfcd400da4383e45e4b8bb189", size = 423956, upload-time = "2025-11-14T20:09:32.876Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/26/ed4498374f9088818278ac225f2bea688b4ec979d81bf83a5355c8c366af/azure_search_documents-11.7.0b2-py3-none-any.whl", hash = "sha256:f82117b321344a84474269ed26df194c24cca619adc024d981b1b86aee3c6f05", size = 432037, upload-time = "2025-11-14T20:09:34.347Z" }, -] - -[[package]] -name = "azure-storage-blob" -version = "12.28.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "cryptography" }, - { name = "isodate" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/24/072ba8e27b0e2d8fec401e9969b429d4f5fc4c8d4f0f05f4661e11f7234a/azure_storage_blob-12.28.0.tar.gz", hash = "sha256:e7d98ea108258d29aa0efbfd591b2e2075fa1722a2fae8699f0b3c9de11eff41", size = 604225, upload-time = "2026-01-06T23:48:57.282Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/3a/6ef2047a072e54e1142718d433d50e9514c999a58f51abfff7902f3a72f8/azure_storage_blob-12.28.0-py3-none-any.whl", hash = "sha256:00fb1db28bf6a7b7ecaa48e3b1d5c83bfadacc5a678b77826081304bd87d6461", size = 431499, upload-time = "2026-01-06T23:48:58.995Z" }, -] - -[[package]] -name = "backoff" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, -] - -[[package]] -name = "blinker" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, -] - -[[package]] -name = "brotli" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" }, - { url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" }, - { url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" }, - { url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" }, - { url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" }, - { url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" }, - { url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" }, - { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" }, - { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" }, - { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" }, - { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" }, - { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" }, - { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" }, - { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" }, - { url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" }, - { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" }, -] - -[[package]] -name = "brotlicffi" -version = "1.2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/84/85/57c314a6b35336efbbdc13e5fc9ae13f6b60a0647cfa7c1221178ac6d8ae/brotlicffi-1.2.0.0.tar.gz", hash = "sha256:34345d8d1f9d534fcac2249e57a4c3c8801a33c9942ff9f8574f67a175e17adb", size = 476682, upload-time = "2025-11-21T18:17:57.334Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/df/a72b284d8c7bef0ed5756b41c2eb7d0219a1dd6ac6762f1c7bdbc31ef3af/brotlicffi-1.2.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:9458d08a7ccde8e3c0afedbf2c70a8263227a68dea5ab13590593f4c0a4fd5f4", size = 432340, upload-time = "2025-11-21T18:17:42.277Z" }, - { url = "https://files.pythonhosted.org/packages/74/2b/cc55a2d1d6fb4f5d458fba44a3d3f91fb4320aa14145799fd3a996af0686/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84e3d0020cf1bd8b8131f4a07819edee9f283721566fe044a20ec792ca8fd8b7", size = 1534002, upload-time = "2025-11-21T18:17:43.746Z" }, - { url = "https://files.pythonhosted.org/packages/e4/9c/d51486bf366fc7d6735f0e46b5b96ca58dc005b250263525a1eea3cd5d21/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33cfb408d0cff64cd50bef268c0fed397c46fbb53944aa37264148614a62e990", size = 1536547, upload-time = "2025-11-21T18:17:45.729Z" }, - { url = "https://files.pythonhosted.org/packages/1b/37/293a9a0a7caf17e6e657668bebb92dfe730305999fe8c0e2703b8888789c/brotlicffi-1.2.0.0-cp38-abi3-win32.whl", hash = "sha256:23e5c912fdc6fd37143203820230374d24babd078fc054e18070a647118158f6", size = 343085, upload-time = "2025-11-21T18:17:48.887Z" }, - { url = "https://files.pythonhosted.org/packages/07/6b/6e92009df3b8b7272f85a0992b306b61c34b7ea1c4776643746e61c380ac/brotlicffi-1.2.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:f139a7cdfe4ae7859513067b736eb44d19fae1186f9e99370092f6915216451b", size = 378586, upload-time = "2025-11-21T18:17:50.531Z" }, -] - -[[package]] -name = "cachetools" -version = "6.2.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731, upload-time = "2025-12-15T18:24:53.744Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" }, -] - -[[package]] -name = "certifi" -version = "2026.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, -] - -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, -] - -[[package]] -name = "click" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, -] - -[[package]] -name = "clr-loader" -version = "0.2.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "python_full_version < '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/61/cf819f8e8bb4d4c74661acf2498ba8d4a296714be3478d21eaabf64f5b9b/clr_loader-0.2.10-py3-none-any.whl", hash = "sha256:ebbbf9d511a7fe95fa28a95a4e04cd195b097881dfe66158dc2c281d3536f282", size = 56483, upload-time = "2026-01-03T23:13:05.439Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "cryptography" -version = "46.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, - { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, - { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, - { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, - { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, - { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, - { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, - { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, - { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, - { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, - { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, - { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, - { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, - { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, - { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, - { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, - { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, - { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, - { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, - { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, - { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, - { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, - { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, - { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, - { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, - { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, -] - -[[package]] -name = "cssselect2" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "tinycss2" }, - { name = "webencodings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/86/fd7f58fc498b3166f3a7e8e0cddb6e620fe1da35b02248b1bd59e95dbaaa/cssselect2-0.8.0.tar.gz", hash = "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a", size = 35716, upload-time = "2025-03-05T14:46:07.988Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/e7/aa315e6a749d9b96c2504a1ba0ba031ba2d0517e972ce22682e3fccecb09/cssselect2-0.8.0-py3-none-any.whl", hash = "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e", size = 15454, upload-time = "2025-03-05T14:46:06.463Z" }, -] - -[[package]] -name = "distro" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, -] - -[[package]] -name = "docstring-parser" -version = "0.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, -] - -[[package]] -name = "fastapi" -version = "0.128.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, -] - -[[package]] -name = "fonttools" -version = "4.61.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, - { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, - { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, - { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, - { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, - { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, - { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, - { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, - { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, - { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, - { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, - { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, - { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, - { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, - { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, - { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, - { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, - { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, - { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, - { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, - { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, -] - -[package.optional-dependencies] -woff = [ - { name = "brotli", marker = "platform_python_implementation == 'CPython'" }, - { name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" }, - { name = "zopfli" }, -] - -[[package]] -name = "frozenlist" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, - { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, - { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, - { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, - { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, - { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, - { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, - { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, - { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, - { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, - { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, - { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, - { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, - { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, - { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, - { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, - { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, - { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, - { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, - { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, - { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, - { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, - { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, - { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, - { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, - { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, - { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, - { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, - { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, - { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, - { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, - { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, - { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, - { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, - { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, -] - -[[package]] -name = "furl" -version = "2.1.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "orderedmultidict" }, - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/53/e4/203a76fa2ef46cdb0a618295cc115220cbb874229d4d8721068335eb87f0/furl-2.1.4.tar.gz", hash = "sha256:877657501266c929269739fb5f5980534a41abd6bbabcb367c136d1d3b2a6015", size = 57526, upload-time = "2025-03-09T05:36:21.175Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/8c/dce3b1b7593858eba995b2dfdb833f872c7f863e3da92aab7128a6b11af4/furl-2.1.4-py2.py3-none-any.whl", hash = "sha256:da34d0b34e53ffe2d2e6851a7085a05d96922b5b578620a37377ff1dbeeb11c8", size = 27550, upload-time = "2025-03-09T05:36:19.928Z" }, -] - -[[package]] -name = "gitdb" -version = "4.0.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "smmap" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, -] - -[[package]] -name = "gitpython" -version = "3.1.46" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "gitdb" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, -] - -[[package]] -name = "google-api-core" -version = "2.29.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-auth" }, - { name = "googleapis-common-protos" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/10/05572d33273292bac49c2d1785925f7bc3ff2fe50e3044cf1062c1dde32e/google_api_core-2.29.0.tar.gz", hash = "sha256:84181be0f8e6b04006df75ddfe728f24489f0af57c96a529ff7cf45bc28797f7", size = 177828, upload-time = "2026-01-08T22:21:39.269Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/b6/85c4d21067220b9a78cfb81f516f9725ea6befc1544ec9bd2c1acd97c324/google_api_core-2.29.0-py3-none-any.whl", hash = "sha256:d30bc60980daa36e314b5d5a3e5958b0200cb44ca8fa1be2b614e932b75a3ea9", size = 173906, upload-time = "2026-01-08T22:21:36.093Z" }, -] - -[[package]] -name = "google-auth" -version = "2.47.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1-modules" }, - { name = "rsa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/60/3c/ec64b9a275ca22fa1cd3b6e77fefcf837b0732c890aa32d2bd21313d9b33/google_auth-2.47.0.tar.gz", hash = "sha256:833229070a9dfee1a353ae9877dcd2dec069a8281a4e72e72f77d4a70ff945da", size = 323719, upload-time = "2026-01-06T21:55:31.045Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl", hash = "sha256:c516d68336bfde7cf0da26aab674a36fedcf04b37ac4edd59c597178760c3498", size = 234867, upload-time = "2026-01-06T21:55:28.6Z" }, -] - -[[package]] -name = "googleapis-common-protos" -version = "1.72.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, -] - -[[package]] -name = "greenlet" -version = "3.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, - { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, - { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, - { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, - { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, - { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, - { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, - { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, - { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387, upload-time = "2025-12-04T14:26:51.063Z" }, - { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, - { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, - { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, - { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, - { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" }, -] - -[[package]] -name = "griffe" -version = "1.15.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, -] - -[[package]] -name = "grpcio" -version = "1.76.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" }, - { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" }, - { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" }, - { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" }, - { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" }, - { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" }, - { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" }, - { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" }, - { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" }, - { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" }, - { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" }, - { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" }, - { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" }, - { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" }, - { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" }, - { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" }, - { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" }, - { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" }, - { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[package]] -name = "h2" -version = "4.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "hpack" }, - { name = "hyperframe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, -] - -[[package]] -name = "hpack" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httptools" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, - { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, - { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, - { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, - { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, - { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, - { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, - { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, - { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, - { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, - { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, - { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, - { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - -[package.optional-dependencies] -http2 = [ - { name = "h2" }, -] - -[[package]] -name = "httpx-sse" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, -] - -[[package]] -name = "hyperframe" -version = "6.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, -] - -[[package]] -name = "importlib-metadata" -version = "8.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, -] - -[[package]] -name = "isodate" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - -[[package]] -name = "jiter" -version = "0.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, - { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, - { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, - { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, - { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, - { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, - { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, - { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, - { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, - { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, - { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, - { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, - { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, - { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, - { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, - { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, - { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, - { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, - { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, - { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, - { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, - { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, - { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, - { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, - { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, - { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, - { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, - { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, - { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, -] - -[[package]] -name = "jsonpath-ng" -version = "1.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ply" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/86/08646239a313f895186ff0a4573452038eed8c86f54380b3ebac34d32fb2/jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c", size = 37838, upload-time = "2024-10-11T15:41:42.404Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/5a/73ecb3d82f8615f32ccdadeb9356726d6cae3a4bbc840b437ceb95708063/jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6", size = 30105, upload-time = "2024-11-20T17:58:30.418Z" }, -] - -[[package]] -name = "jsonschema" -version = "4.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, -] - -[[package]] -name = "lxml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, - { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, - { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, - { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, - { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, - { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, - { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, - { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, - { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, - { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, - { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, - { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, - { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, - { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, - { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, - { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, - { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, - { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, - { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, - { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, - { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, - { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, - { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, - { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, - { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, - { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, - { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, - { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, - { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, - { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, - { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, - { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, - { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, - { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, - { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, - { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, -] - -[[package]] -name = "markdown" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, -] - -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, -] - -[[package]] -name = "mcp" -version = "1.25.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "jsonschema" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "python-multipart" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, - { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387, upload-time = "2025-12-19T10:19:56.985Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076, upload-time = "2025-12-19T10:19:55.416Z" }, -] - -[package.optional-dependencies] -ws = [ - { name = "websockets" }, -] - -[[package]] -name = "mem0ai" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "openai" }, - { name = "posthog" }, - { name = "protobuf" }, - { name = "pydantic" }, - { name = "pytz" }, - { name = "qdrant-client" }, - { name = "sqlalchemy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/39/cd/f9047cd45952af08da8084c2297f8aad780f9ac8558631fc64b3ed235b28/mem0ai-1.0.1.tar.gz", hash = "sha256:53be77f479387e6c07508096eb6c0688150b31152613bdcf6c281246b000b14d", size = 182296, upload-time = "2025-11-13T22:32:13.658Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/42/120d6db33e190ef09d69428ddd2eaaa87e10f4c8243af788f5fc524748c9/mem0ai-1.0.1-py3-none-any.whl", hash = "sha256:a8eeca9688e87f175af53d463b4a3b2d552984c81e29bc656c847dc04eaf6f75", size = 275351, upload-time = "2025-11-13T22:32:11.839Z" }, -] - -[[package]] -name = "microsoft-agents-activity" -version = "0.7.0.dev12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/92/02/9c9eb63917392883ad371f1f8c534adfb68deeb0a2ffcf489951a3a5ebc6/microsoft_agents_activity-0.7.0.dev12.tar.gz", hash = "sha256:0b3d7ca7af9559729e32aa2c64aef6de4426a0d8357af7a55f5a8cded5d084a9", size = 60983, upload-time = "2025-12-17T09:06:38.39Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/71/e946dfe26df5c57487c587b95e05c77f39a6a75181caad0a2e47fe2b0b70/microsoft_agents_activity-0.7.0.dev12-py3-none-any.whl", hash = "sha256:fb87ce08abe35e7e1226db34a76a2a6303989fa4f6ee3f82b39c51440d999cd8", size = 132661, upload-time = "2025-12-17T09:06:47.842Z" }, -] - -[[package]] -name = "microsoft-agents-copilotstudio-client" -version = "0.7.0.dev12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "microsoft-agents-hosting-core" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/88/e2/ee2077a873377c3d6832fb44c339dd2b1db9b65e53e9cdd1b460daa8aef2/microsoft_agents_copilotstudio_client-0.7.0.dev12.tar.gz", hash = "sha256:cab5c1bc149bbd3b32ce3f00ecdb38ff00664f180d93f882a5e65fa738d6ff88", size = 12648, upload-time = "2025-12-17T09:06:40.68Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/67/4d01168a35c4dd7ed97f9588143e3eea8f4894ce5de8401428da0dad3fc9/microsoft_agents_copilotstudio_client-0.7.0.dev12-py3-none-any.whl", hash = "sha256:c78682deb416652957992436b47c864c4287da377fe48fcd2bfef3eacf99cc75", size = 13494, upload-time = "2025-12-17T09:06:50.139Z" }, -] - -[[package]] -name = "microsoft-agents-hosting-core" -version = "0.7.0.dev12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "isodate" }, - { name = "microsoft-agents-activity" }, - { name = "pyjwt" }, - { name = "python-dotenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1e/ca/ffe2f0ed6aa9f7a2a9d793539003fee0e86622b83a61f0933065de7b7953/microsoft_agents_hosting_core-0.7.0.dev12.tar.gz", hash = "sha256:8093ced5a435cb2fb177be38dd1eeaec937aefa544ec1371f65b41dd53a3721d", size = 90609, upload-time = "2025-12-17T09:06:42.622Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/19/b09facedd92b439ffed675ceaf611bd9107acfcf77df531816587019f865/microsoft_agents_hosting_core-0.7.0.dev12-py3-none-any.whl", hash = "sha256:cca0d752c8ce055cc53211e0e3e501466ac629bf50f391550c9f029b791b620e", size = 133796, upload-time = "2025-12-17T09:06:51.841Z" }, -] - -[[package]] -name = "ml-dtypes" -version = "0.5.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888, upload-time = "2025-11-17T22:31:56.907Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993, upload-time = "2025-11-17T22:31:58.497Z" }, - { url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956, upload-time = "2025-11-17T22:31:59.931Z" }, - { url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224, upload-time = "2025-11-17T22:32:01.349Z" }, - { url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798, upload-time = "2025-11-17T22:32:02.864Z" }, - { url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083, upload-time = "2025-11-17T22:32:04.08Z" }, - { url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111, upload-time = "2025-11-17T22:32:05.546Z" }, - { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453, upload-time = "2025-11-17T22:32:07.115Z" }, - { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612, upload-time = "2025-11-17T22:32:08.615Z" }, - { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145, upload-time = "2025-11-17T22:32:09.782Z" }, - { url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781, upload-time = "2025-11-17T22:32:11.364Z" }, - { url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145, upload-time = "2025-11-17T22:32:12.783Z" }, - { url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230, upload-time = "2025-11-17T22:32:14.38Z" }, - { url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032, upload-time = "2025-11-17T22:32:15.763Z" }, - { url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353, upload-time = "2025-11-17T22:32:16.932Z" }, - { url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085, upload-time = "2025-11-17T22:32:18.175Z" }, - { url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358, upload-time = "2025-11-17T22:32:19.7Z" }, - { url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332, upload-time = "2025-11-17T22:32:21.193Z" }, - { url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612, upload-time = "2025-11-17T22:32:22.579Z" }, - { url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825, upload-time = "2025-11-17T22:32:23.766Z" }, -] - -[[package]] -name = "msal" -version = "1.35.0b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/7a/6880016fab1720981b54db844c32af6f2e5e90aac21575ad6e54e1840313/msal-1.35.0b1.tar.gz", hash = "sha256:fe8143079183a5c952cd9f3ba66a148fe7bae9fb9952bd0e834272bfbeb34508", size = 157573, upload-time = "2026-01-06T23:51:56.958Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/8e/7090fafcf58e9081767a8fa960431c708211ce273bc4f6e519e9046acacc/msal-1.35.0b1-py3-none-any.whl", hash = "sha256:bf656775c64bbc2103d8255980f5c3c966c7432106795e1fe70ca338a7e43150", size = 117733, upload-time = "2026-01-06T23:51:55.903Z" }, -] - -[[package]] -name = "msal-extensions" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "msal" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, -] - -[[package]] -name = "msrest" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "certifi" }, - { name = "isodate" }, - { name = "requests" }, - { name = "requests-oauthlib" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/68/77/8397c8fb8fc257d8ea0fa66f8068e073278c65f05acb17dcb22a02bfdc42/msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9", size = 175332, upload-time = "2022-06-13T22:41:25.111Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/cf/f2966a2638144491f8696c27320d5219f48a072715075d168b31d3237720/msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32", size = 85384, upload-time = "2022-06-13T22:41:22.42Z" }, -] - -[[package]] -name = "multidict" -version = "6.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, - { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, - { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, - { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, - { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, - { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, - { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, - { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, - { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, - { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, - { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, - { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, - { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, - { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, - { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, - { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, - { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, - { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, - { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, - { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, - { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, - { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, - { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, - { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, - { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, - { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, - { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, - { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, - { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, - { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, - { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, - { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, - { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, - { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, - { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, - { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, - { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, - { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, - { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, - { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, - { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, - { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, - { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, - { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, - { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, - { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, - { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, - { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, - { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, - { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, -] - -[[package]] -name = "narwhals" -version = "2.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/6d/b57c64e5038a8cf071bce391bb11551657a74558877ac961e7fa905ece27/narwhals-2.15.0.tar.gz", hash = "sha256:a9585975b99d95084268445a1fdd881311fa26ef1caa18020d959d5b2ff9a965", size = 603479, upload-time = "2026-01-06T08:10:13.27Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-py3-none-any.whl", hash = "sha256:cbfe21ca19d260d9fd67f995ec75c44592d1f106933b03ddd375df7ac841f9d6", size = 432856, upload-time = "2026-01-06T08:10:11.511Z" }, -] - -[[package]] -name = "numpy" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/68/732d4b7811c00775f3bd522a21e8dd5a23f77eb11acdeb663e4a4ebf0ef4/numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b", size = 16652495, upload-time = "2026-01-10T06:43:06.283Z" }, - { url = "https://files.pythonhosted.org/packages/20/ca/857722353421a27f1465652b2c66813eeeccea9d76d5f7b74b99f298e60e/numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f", size = 12368657, upload-time = "2026-01-10T06:43:09.094Z" }, - { url = "https://files.pythonhosted.org/packages/81/0d/2377c917513449cc6240031a79d30eb9a163d32a91e79e0da47c43f2c0c8/numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9", size = 5197256, upload-time = "2026-01-10T06:43:13.634Z" }, - { url = "https://files.pythonhosted.org/packages/17/39/569452228de3f5de9064ac75137082c6214be1f5c532016549a7923ab4b5/numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e", size = 6545212, upload-time = "2026-01-10T06:43:15.661Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/77333f4d1e4dac4395385482557aeecf4826e6ff517e32ca48e1dafbe42a/numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5", size = 14402871, upload-time = "2026-01-10T06:43:17.324Z" }, - { url = "https://files.pythonhosted.org/packages/ba/87/d341e519956273b39d8d47969dd1eaa1af740615394fe67d06f1efa68773/numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8", size = 16359305, upload-time = "2026-01-10T06:43:19.376Z" }, - { url = "https://files.pythonhosted.org/packages/32/91/789132c6666288eaa20ae8066bb99eba1939362e8f1a534949a215246e97/numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c", size = 16181909, upload-time = "2026-01-10T06:43:21.808Z" }, - { url = "https://files.pythonhosted.org/packages/cf/b8/090b8bd27b82a844bb22ff8fdf7935cb1980b48d6e439ae116f53cdc2143/numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2", size = 18284380, upload-time = "2026-01-10T06:43:23.957Z" }, - { url = "https://files.pythonhosted.org/packages/67/78/722b62bd31842ff029412271556a1a27a98f45359dea78b1548a3a9996aa/numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d", size = 5957089, upload-time = "2026-01-10T06:43:27.535Z" }, - { url = "https://files.pythonhosted.org/packages/da/a6/cf32198b0b6e18d4fbfa9a21a992a7fca535b9bb2b0cdd217d4a3445b5ca/numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb", size = 12307230, upload-time = "2026-01-10T06:43:29.298Z" }, - { url = "https://files.pythonhosted.org/packages/44/6c/534d692bfb7d0afe30611320c5fb713659dcb5104d7cc182aff2aea092f5/numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5", size = 10313125, upload-time = "2026-01-10T06:43:31.782Z" }, - { url = "https://files.pythonhosted.org/packages/da/a1/354583ac5c4caa566de6ddfbc42744409b515039e085fab6e0ff942e0df5/numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7", size = 12496156, upload-time = "2026-01-10T06:43:34.237Z" }, - { url = "https://files.pythonhosted.org/packages/51/b0/42807c6e8cce58c00127b1dc24d365305189991f2a7917aa694a109c8d7d/numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d", size = 5324663, upload-time = "2026-01-10T06:43:36.211Z" }, - { url = "https://files.pythonhosted.org/packages/fe/55/7a621694010d92375ed82f312b2f28017694ed784775269115323e37f5e2/numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15", size = 6645224, upload-time = "2026-01-10T06:43:37.884Z" }, - { url = "https://files.pythonhosted.org/packages/50/96/9fa8635ed9d7c847d87e30c834f7109fac5e88549d79ef3324ab5c20919f/numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9", size = 14462352, upload-time = "2026-01-10T06:43:39.479Z" }, - { url = "https://files.pythonhosted.org/packages/03/d1/8cf62d8bb2062da4fb82dd5d49e47c923f9c0738032f054e0a75342faba7/numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2", size = 16407279, upload-time = "2026-01-10T06:43:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/86/1c/95c86e17c6b0b31ce6ef219da00f71113b220bcb14938c8d9a05cee0ff53/numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505", size = 16248316, upload-time = "2026-01-10T06:43:44.121Z" }, - { url = "https://files.pythonhosted.org/packages/30/b4/e7f5ff8697274c9d0fa82398b6a372a27e5cef069b37df6355ccb1f1db1a/numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2", size = 18329884, upload-time = "2026-01-10T06:43:46.613Z" }, - { url = "https://files.pythonhosted.org/packages/37/a4/b073f3e9d77f9aec8debe8ca7f9f6a09e888ad1ba7488f0c3b36a94c03ac/numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4", size = 6081138, upload-time = "2026-01-10T06:43:48.854Z" }, - { url = "https://files.pythonhosted.org/packages/16/16/af42337b53844e67752a092481ab869c0523bc95c4e5c98e4dac4e9581ac/numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510", size = 12447478, upload-time = "2026-01-10T06:43:50.476Z" }, - { url = "https://files.pythonhosted.org/packages/6c/f8/fa85b2eac68ec631d0b631abc448552cb17d39afd17ec53dcbcc3537681a/numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261", size = 10382981, upload-time = "2026-01-10T06:43:52.575Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a7/ef08d25698e0e4b4efbad8d55251d20fe2a15f6d9aa7c9b30cd03c165e6f/numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", size = 16652046, upload-time = "2026-01-10T06:43:54.797Z" }, - { url = "https://files.pythonhosted.org/packages/8f/39/e378b3e3ca13477e5ac70293ec027c438d1927f18637e396fe90b1addd72/numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", size = 12378858, upload-time = "2026-01-10T06:43:57.099Z" }, - { url = "https://files.pythonhosted.org/packages/c3/74/7ec6154f0006910ed1fdbb7591cf4432307033102b8a22041599935f8969/numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", size = 5207417, upload-time = "2026-01-10T06:43:59.037Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b7/053ac11820d84e42f8feea5cb81cc4fcd1091499b45b1ed8c7415b1bf831/numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", size = 6542643, upload-time = "2026-01-10T06:44:01.852Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c4/2e7908915c0e32ca636b92e4e4a3bdec4cb1e7eb0f8aedf1ed3c68a0d8cd/numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", size = 14418963, upload-time = "2026-01-10T06:44:04.047Z" }, - { url = "https://files.pythonhosted.org/packages/eb/c0/3ed5083d94e7ffd7c404e54619c088e11f2e1939a9544f5397f4adb1b8ba/numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", size = 16363811, upload-time = "2026-01-10T06:44:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/0e/68/42b66f1852bf525050a67315a4fb94586ab7e9eaa541b1bef530fab0c5dd/numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", size = 16197643, upload-time = "2026-01-10T06:44:08.33Z" }, - { url = "https://files.pythonhosted.org/packages/d2/40/e8714fc933d85f82c6bfc7b998a0649ad9769a32f3494ba86598aaf18a48/numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", size = 18289601, upload-time = "2026-01-10T06:44:10.841Z" }, - { url = "https://files.pythonhosted.org/packages/80/9a/0d44b468cad50315127e884802351723daca7cf1c98d102929468c81d439/numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745", size = 6005722, upload-time = "2026-01-10T06:44:13.332Z" }, - { url = "https://files.pythonhosted.org/packages/7e/bb/c6513edcce5a831810e2dddc0d3452ce84d208af92405a0c2e58fd8e7881/numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d", size = 12438590, upload-time = "2026-01-10T06:44:15.006Z" }, - { url = "https://files.pythonhosted.org/packages/e9/da/a598d5cb260780cf4d255102deba35c1d072dc028c4547832f45dd3323a8/numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df", size = 10596180, upload-time = "2026-01-10T06:44:17.386Z" }, - { url = "https://files.pythonhosted.org/packages/de/bc/ea3f2c96fcb382311827231f911723aeff596364eb6e1b6d1d91128aa29b/numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", size = 12498774, upload-time = "2026-01-10T06:44:19.467Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ab/ef9d939fe4a812648c7a712610b2ca6140b0853c5efea361301006c02ae5/numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", size = 5327274, upload-time = "2026-01-10T06:44:23.189Z" }, - { url = "https://files.pythonhosted.org/packages/bd/31/d381368e2a95c3b08b8cf7faac6004849e960f4a042d920337f71cef0cae/numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c", size = 6648306, upload-time = "2026-01-10T06:44:25.012Z" }, - { url = "https://files.pythonhosted.org/packages/c8/e5/0989b44ade47430be6323d05c23207636d67d7362a1796ccbccac6773dd2/numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", size = 14464653, upload-time = "2026-01-10T06:44:26.706Z" }, - { url = "https://files.pythonhosted.org/packages/10/a7/cfbe475c35371cae1358e61f20c5f075badc18c4797ab4354140e1d283cf/numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", size = 16405144, upload-time = "2026-01-10T06:44:29.378Z" }, - { url = "https://files.pythonhosted.org/packages/f8/a3/0c63fe66b534888fa5177cc7cef061541064dbe2b4b60dcc60ffaf0d2157/numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", size = 16247425, upload-time = "2026-01-10T06:44:31.721Z" }, - { url = "https://files.pythonhosted.org/packages/6b/2b/55d980cfa2c93bd40ff4c290bf824d792bd41d2fe3487b07707559071760/numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", size = 18330053, upload-time = "2026-01-10T06:44:34.617Z" }, - { url = "https://files.pythonhosted.org/packages/23/12/8b5fc6b9c487a09a7957188e0943c9ff08432c65e34567cabc1623b03a51/numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", size = 6152482, upload-time = "2026-01-10T06:44:36.798Z" }, - { url = "https://files.pythonhosted.org/packages/00/a5/9f8ca5856b8940492fc24fbe13c1bc34d65ddf4079097cf9e53164d094e1/numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", size = 12627117, upload-time = "2026-01-10T06:44:38.828Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121, upload-time = "2026-01-10T06:44:41.644Z" }, -] - -[[package]] -name = "oauthlib" -version = "3.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, -] - -[[package]] -name = "ollama" -version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/5a/652dac4b7affc2b37b95386f8ae78f22808af09d720689e3d7a86b6ed98e/ollama-0.6.1.tar.gz", hash = "sha256:478c67546836430034b415ed64fa890fd3d1ff91781a9d548b3325274e69d7c6", size = 51620, upload-time = "2025-11-13T23:02:17.416Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/4f/4a617ee93d8208d2bcf26b2d8b9402ceaed03e3853c754940e2290fed063/ollama-0.6.1-py3-none-any.whl", hash = "sha256:fc4c984b345735c5486faeee67d8a265214a31cbb828167782dc642ce0a2bf8c", size = 14354, upload-time = "2025-11-13T23:02:16.292Z" }, -] - -[[package]] -name = "openai" -version = "2.15.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "tqdm" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/f4/4690ecb5d70023ce6bfcfeabfe717020f654bde59a775058ec6ac4692463/openai-2.15.0.tar.gz", hash = "sha256:42eb8cbb407d84770633f31bf727d4ffb4138711c670565a41663d9439174fba", size = 627383, upload-time = "2026-01-09T22:10:08.603Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/df/c306f7375d42bafb379934c2df4c2fa3964656c8c782bac75ee10c102818/openai-2.15.0-py3-none-any.whl", hash = "sha256:6ae23b932cd7230f7244e52954daa6602716d6b9bf235401a107af731baea6c3", size = 1067879, upload-time = "2026-01-09T22:10:06.446Z" }, -] - -[[package]] -name = "openai-agents" -version = "0.6.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "griffe" }, - { name = "mcp" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "types-requests" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e7/5c/5ebface62a0efdc7298152dcd2d32164403e25e53f1088c042936d8d40f9/openai_agents-0.6.5.tar.gz", hash = "sha256:67e8cab27082d1a1fe6f3fecfcf89b41ff249988a75640bbcc2764952d603ef0", size = 2044506, upload-time = "2026-01-06T15:32:50.936Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/db/16020e45d53366f2ed653ce0ddf959a647687d47180954de7654a133b910/openai_agents-0.6.5-py3-none-any.whl", hash = "sha256:c81d2eaa5c4563b8e893ba836fe170cf10ba974420ff283b4f001f84e7cb6e6b", size = 249352, upload-time = "2026-01-06T15:32:48.847Z" }, -] - -[[package]] -name = "openai-chatkit" -version = "1.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "openai" }, - { name = "openai-agents" }, - { name = "pydantic" }, - { name = "uvicorn" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/f3/3e7aafd6c29348e60d32082fb14e539661fe4100453a31b34d0fef1ff7b7/openai_chatkit-1.5.2.tar.gz", hash = "sha256:187d27b815f153fa060337c86ee3aab189f72269f23ac2bb2a35c6c88b83846d", size = 59268, upload-time = "2026-01-10T00:59:41.215Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/b6/475a4c723fb2e0de30feea505505eabe77666aa7d81855d356fb289e3d8a/openai_chatkit-1.5.2-py3-none-any.whl", hash = "sha256:3bf3f140f314924ef1d4148ce5174cff6aa4c5d1760f988ba2aa267fd434f960", size = 41482, upload-time = "2026-01-10T00:59:40.023Z" }, -] - -[[package]] -name = "opentelemetry-api" -version = "1.39.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/0b/e5428c009d4d9af0515b0a8371a8aaae695371af291f45e702f7969dce6b/opentelemetry_api-1.39.0.tar.gz", hash = "sha256:6130644268c5ac6bdffaf660ce878f10906b3e789f7e2daa5e169b047a2933b9", size = 65763, upload-time = "2025-12-03T13:19:56.378Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/85/d831a9bc0a9e0e1a304ff3d12c1489a5fbc9bf6690a15dcbdae372bbca45/opentelemetry_api-1.39.0-py3-none-any.whl", hash = "sha256:3c3b3ca5c5687b1b5b37e5c5027ff68eacea8675241b29f13110a8ffbb8f0459", size = 66357, upload-time = "2025-12-03T13:19:33.043Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation" -version = "0.60b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "packaging" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/3c/bd53dbb42eff93d18e3047c7be11224aa9966ce98ac4cc5bfb860a32c95a/opentelemetry_instrumentation-0.60b0.tar.gz", hash = "sha256:4e9fec930f283a2677a2217754b40aaf9ef76edae40499c165bc7f1d15366a74", size = 31707, upload-time = "2025-12-03T13:22:00.352Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/7b/5b5b9f8cfe727a28553acf9cd287b1d7f706f5c0a00d6e482df55b169483/opentelemetry_instrumentation-0.60b0-py3-none-any.whl", hash = "sha256:aaafa1483543a402819f1bdfb06af721c87d60dd109501f9997332862a35c76a", size = 33096, upload-time = "2025-12-03T13:20:51.785Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-asgi" -version = "0.60b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "asgiref" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b0/0a/715ea7044708d3c215385fb2a1c6ffe429aacb3cd23a348060aaeda52834/opentelemetry_instrumentation_asgi-0.60b0.tar.gz", hash = "sha256:928731218050089dca69f0fe980b8bfe109f384be8b89802d7337372ddb67b91", size = 26083, upload-time = "2025-12-03T13:22:05.672Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/8c/c6c59127fd996107243ca45669355665a7daff578ddafb86d6d2d3b01428/opentelemetry_instrumentation_asgi-0.60b0-py3-none-any.whl", hash = "sha256:9d76a541269452c718a0384478f3291feb650c5a3f29e578fdc6613ea3729cf3", size = 16907, upload-time = "2025-12-03T13:20:58.962Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-dbapi" -version = "0.60b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/12/7f/b4c1fbce01b29daad5ef1396427c9cd3c7a55ee68e75f8c11089c7e2533d/opentelemetry_instrumentation_dbapi-0.60b0.tar.gz", hash = "sha256:2b7eb38e46890cebe5bc1a1c03d2ab07fc159b0b7b91342941ee33dd73876d84", size = 16311, upload-time = "2025-12-03T13:22:15.369Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/0a/65e100c6d803de59a9113a993dcd371a4027453ba15ce4dabdb0343ca154/opentelemetry_instrumentation_dbapi-0.60b0-py3-none-any.whl", hash = "sha256:429d8ca34a44a4296b9b09a1bd373fff350998d200525c6e79883c3328559b03", size = 13966, upload-time = "2025-12-03T13:21:12.435Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-django" -version = "0.60b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-instrumentation-wsgi" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7c/d2/8ddd9a5c61cd5048d422be8d22fac40f603aa82f0babf9f7c40db871080c/opentelemetry_instrumentation_django-0.60b0.tar.gz", hash = "sha256:461e6fca27936ba97eec26da38bb5f19310783370478c7ca3a3e40faaceac9cc", size = 26596, upload-time = "2025-12-03T13:22:16.069Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/d6/28684547bf6c699582e998a172ba8bb08405cf6706729b0d6a16042e998f/opentelemetry_instrumentation_django-0.60b0-py3-none-any.whl", hash = "sha256:95495649c8c34ce9217c6873cdd10fc4fcaa67c25f8329adc54f5b286999e40b", size = 21169, upload-time = "2025-12-03T13:21:13.475Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-fastapi" -version = "0.60b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-instrumentation-asgi" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fe/51/a021a7c929b5103fcb6bfdfa5a99abcaeb3b505faf9e3ee3ec14612c1ef9/opentelemetry_instrumentation_fastapi-0.60b0.tar.gz", hash = "sha256:5d34d67eb634a08bfe9e530680d6177521cd9da79285144e6d5a8f42683ed1b3", size = 24960, upload-time = "2025-12-03T13:22:18.468Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/5a/e238c108eb65a726d75184439377a87d532050036b54e718e4c789b26d1a/opentelemetry_instrumentation_fastapi-0.60b0-py3-none-any.whl", hash = "sha256:415c6602db01ee339276ea4cabe3e80177c9e955631c087f2ef60a75e31bfaee", size = 13478, upload-time = "2025-12-03T13:21:16.804Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-flask" -version = "0.60b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-instrumentation-wsgi" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/30/cc/e0758c23d66fd49956169cb24b5b06130373da2ce8d49945abce82003518/opentelemetry_instrumentation_flask-0.60b0.tar.gz", hash = "sha256:560f08598ef40cdcf7ca05bfb2e3ea74fab076e676f4c18bb36bb379bf5c4a1b", size = 20336, upload-time = "2025-12-03T13:22:19.162Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/b5/387ce11f59e5ce65b890adc3f9c457877143b8a6d107a3a0b305397933a1/opentelemetry_instrumentation_flask-0.60b0-py3-none-any.whl", hash = "sha256:106e5774f79ac9b86dd0d949c1b8f46c807a8af16184301e10d24fc94e680d04", size = 15189, upload-time = "2025-12-03T13:21:18.672Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-psycopg2" -version = "0.60b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-instrumentation-dbapi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f8/68/5ae8a3b9a28c2fdf8d3d050e451ddb2612ca963679b08a2959f01f6dda4b/opentelemetry_instrumentation_psycopg2-0.60b0.tar.gz", hash = "sha256:59e527fd97739440380634ffcf9431aa7f2965d939d8d5829790886e2b54ede9", size = 11266, upload-time = "2025-12-03T13:22:26.025Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/66b5a41a2b0d1d07cc9b0fbd80f8b5c66b46a4d4731743505891da8b3cbe/opentelemetry_instrumentation_psycopg2-0.60b0-py3-none-any.whl", hash = "sha256:ea136a32babd559aa717c04dddf6aa78aa94b816fb4e10dfe06751727ef306d4", size = 11284, upload-time = "2025-12-03T13:21:31.23Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-requests" -version = "0.60b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/26/0f/94c6181e95c867f559715887c418170a9eadd92ea6090122d464e375ff56/opentelemetry_instrumentation_requests-0.60b0.tar.gz", hash = "sha256:5079ed8df96d01dab915a0766cd28a49be7c33439ce43d6d39843ed6dee3204f", size = 16173, upload-time = "2025-12-03T13:22:31.458Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/e1/2f13b41c5679243ba8eae651170c4ce2f532349877819566ae4a89a2b47f/opentelemetry_instrumentation_requests-0.60b0-py3-none-any.whl", hash = "sha256:e9957f3a650ae55502fa227b29ff985b37d63e41c85e6e1555d48039f092ea83", size = 13122, upload-time = "2025-12-03T13:21:38.983Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-urllib" -version = "0.60b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/db/be895de04bd56d7a2b2ef6d267a4c52f6cd325b6647d1c15ae888b1b0f6a/opentelemetry_instrumentation_urllib-0.60b0.tar.gz", hash = "sha256:89b8796f9ab64d0ea0833cfea98745963baa0d7e4a775b3d2a77791aa97cf3f9", size = 13931, upload-time = "2025-12-03T13:22:37.44Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/e0/178914d5cec77baef797c6d47412da478ff871b05eb8732d64037b87c868/opentelemetry_instrumentation_urllib-0.60b0-py3-none-any.whl", hash = "sha256:80e3545d02505dc0ea61b3a0a141ec2828e11bee6b7dedfd3ee7ed9a7adbf862", size = 12673, upload-time = "2025-12-03T13:21:48.139Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-urllib3" -version = "0.60b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/25/a8/16a32239e84741fae1a2932badeade5e72b73bfc331b53f7049a648ca00b/opentelemetry_instrumentation_urllib3-0.60b0.tar.gz", hash = "sha256:6ae1640a993901bae8eda5496d8b1440fb326a29e4ba1db342738b8868174aad", size = 15789, upload-time = "2025-12-03T13:22:38.073Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/b2/ca27479eaf1f3f4825481769eb0cb200cad839040b8d5f42662d0398a256/opentelemetry_instrumentation_urllib3-0.60b0-py3-none-any.whl", hash = "sha256:9a07504560feae650a9205b3e2a579a835819bb1d55498d26a5db477fe04bba0", size = 13187, upload-time = "2025-12-03T13:21:49.482Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation-wsgi" -version = "0.60b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/10/ad/ae04e35f3b96d9c20d5d3df94a4c296eabf7a54d35d6c831179471128270/opentelemetry_instrumentation_wsgi-0.60b0.tar.gz", hash = "sha256:5815195b1b9890f55c4baafec94ff98591579a7d9b16256064adea8ee5784651", size = 19104, upload-time = "2025-12-03T13:22:38.733Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/0e/1ed4d3cdce7b2e00a24f79933b3472e642d4db98aaccc09769be5cbe5296/opentelemetry_instrumentation_wsgi-0.60b0-py3-none-any.whl", hash = "sha256:0ff80614c1e73f7e94a5860c7e6222a51195eebab3dc5f50d89013db3d5d2f13", size = 14553, upload-time = "2025-12-03T13:21:50.491Z" }, -] - -[[package]] -name = "opentelemetry-resource-detector-azure" -version = "0.1.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-sdk" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/67/e4/0d359d48d03d447225b30c3dd889d5d454e3b413763ff721f9b0e4ac2e59/opentelemetry_resource_detector_azure-0.1.5.tar.gz", hash = "sha256:e0ba658a87c69eebc806e75398cd0e9f68a8898ea62de99bc1b7083136403710", size = 11503, upload-time = "2024-05-16T21:54:58.994Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/ae/c26d8da88ba2e438e9653a408b0c2ad6f17267801250a8f3cc6405a93a72/opentelemetry_resource_detector_azure-0.1.5-py3-none-any.whl", hash = "sha256:4dcc5d54ab5c3b11226af39509bc98979a8b9e0f8a24c1b888783755d3bf00eb", size = 14252, upload-time = "2024-05-16T21:54:57.208Z" }, -] - -[[package]] -name = "opentelemetry-sdk" -version = "1.39.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/51/e3/7cd989003e7cde72e0becfe830abff0df55c69d237ee7961a541e0167833/opentelemetry_sdk-1.39.0.tar.gz", hash = "sha256:c22204f12a0529e07aa4d985f1bca9d6b0e7b29fe7f03e923548ae52e0e15dde", size = 171322, upload-time = "2025-12-03T13:20:09.651Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/b4/2adc8bc83eb1055ecb592708efb6f0c520cc2eb68970b02b0f6ecda149cf/opentelemetry_sdk-1.39.0-py3-none-any.whl", hash = "sha256:90cfb07600dfc0d2de26120cebc0c8f27e69bf77cd80ef96645232372709a514", size = 132413, upload-time = "2025-12-03T13:19:51.364Z" }, -] - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.60b0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/0e/176a7844fe4e3cb5de604212094dffaed4e18b32f1c56b5258bcbcba85c2/opentelemetry_semantic_conventions-0.60b0.tar.gz", hash = "sha256:227d7aa73cbb8a2e418029d6b6465553aa01cf7e78ec9d0bc3255c7b3ac5bf8f", size = 137935, upload-time = "2025-12-03T13:20:12.395Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/56/af0306666f91bae47db14d620775604688361f0f76a872e0005277311131/opentelemetry_semantic_conventions-0.60b0-py3-none-any.whl", hash = "sha256:069530852691136018087b52688857d97bba61cd641d0f8628d2d92788c4f78a", size = 219981, upload-time = "2025-12-03T13:19:53.585Z" }, -] - -[[package]] -name = "opentelemetry-semantic-conventions-ai" -version = "0.4.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e6/40b59eda51ac47009fb47afcdf37c6938594a0bd7f3b9fadcbc6058248e3/opentelemetry_semantic_conventions_ai-0.4.13.tar.gz", hash = "sha256:94efa9fb4ffac18c45f54a3a338ffeb7eedb7e1bb4d147786e77202e159f0036", size = 5368, upload-time = "2025-08-22T10:14:17.387Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/b5/cf25da2218910f0d6cdf7f876a06bed118c4969eacaf60a887cbaef44f44/opentelemetry_semantic_conventions_ai-0.4.13-py3-none-any.whl", hash = "sha256:883a30a6bb5deaec0d646912b5f9f6dcbb9f6f72557b73d0f2560bf25d13e2d5", size = 6080, upload-time = "2025-08-22T10:14:16.477Z" }, -] - -[[package]] -name = "opentelemetry-util-http" -version = "0.60b0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/38/0d/786a713445cf338131fef3a84fab1378e4b2ef3c3ea348eeb0c915eb804a/opentelemetry_util_http-0.60b0.tar.gz", hash = "sha256:e42b7bb49bba43b6f34390327d97e5016eb1c47949ceaf37c4795472a4e3a82d", size = 10576, upload-time = "2025-12-03T13:22:41.224Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/5d/a448862f6d10c95685ed0e703596b6bd1784074e7ad90bffdc550abb7b68/opentelemetry_util_http-0.60b0-py3-none-any.whl", hash = "sha256:4f366f1a48adb74ffa6f80aee26f96882e767e01b03cd1cfb948b6e1020341fe", size = 8742, upload-time = "2025-12-03T13:21:54.553Z" }, -] - -[[package]] -name = "orderedmultidict" -version = "1.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5c/62/61ad51f6c19d495970230a7747147ce7ed3c3a63c2af4ebfdb1f6d738703/orderedmultidict-1.0.2.tar.gz", hash = "sha256:16a7ae8432e02cc987d2d6d5af2df5938258f87c870675c73ee77a0920e6f4a6", size = 13973, upload-time = "2025-11-18T08:00:42.649Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/6c/d8a02ffb24876b5f51fbd781f479fc6525a518553a4196bd0433dae9ff8e/orderedmultidict-1.0.2-py2.py3-none-any.whl", hash = "sha256:ab5044c1dca4226ae4c28524cfc5cc4c939f0b49e978efa46a6ad6468049f79b", size = 11897, upload-time = "2025-11-18T08:00:41.44Z" }, -] - -[[package]] -name = "packaging" -version = "26.0rc1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/55/d0/88784ecdb0e481b39af721f637a60046e6f09ca03553aa71d788062e2012/packaging-26.0rc1.tar.gz", hash = "sha256:2104df24f61f17179ac8459cda8138cd344967d3b4f0934afa582a6826963fc5", size = 142273, upload-time = "2026-01-09T17:41:18.505Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/35/ddf3a6e8fc754fb939e2ea36fde96c28189184d6115afcf60011bb438ae5/packaging-26.0rc1-py3-none-any.whl", hash = "sha256:ecf921b33c620e357b1eed2ac3bc6313b1582874b0282d0773b6797b79cb0786", size = 74021, upload-time = "2026-01-09T17:41:17.134Z" }, -] - -[[package]] -name = "pandas" -version = "2.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, - { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, - { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, - { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, - { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, - { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, - { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, - { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, - { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, - { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, - { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, - { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, - { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, - { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, - { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, - { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, - { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, - { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, - { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, - { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, - { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, - { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, - { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, -] - -[[package]] -name = "pillow" -version = "12.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" }, - { url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" }, - { url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" }, - { url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" }, - { url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" }, - { url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" }, - { url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" }, - { url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" }, - { url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" }, - { url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" }, - { url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" }, - { url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" }, - { url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" }, - { url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" }, - { url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" }, - { url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" }, - { url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" }, - { url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" }, - { url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" }, - { url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" }, - { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, - { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, - { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, - { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, - { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, - { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, - { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, - { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, - { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, - { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" }, - { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" }, - { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" }, - { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, - { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, - { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, - { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, - { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, - { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, - { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, - { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, -] - -[[package]] -name = "plotly" -version = "6.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "narwhals" }, - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e3/4f/8a10a9b9f5192cb6fdef62f1d77fa7d834190b2c50c0cd256bd62879212b/plotly-6.5.2.tar.gz", hash = "sha256:7478555be0198562d1435dee4c308268187553cc15516a2f4dd034453699e393", size = 7015695, upload-time = "2026-01-14T21:26:51.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/67/f95b5460f127840310d2187f916cf0023b5875c0717fdf893f71e1325e87/plotly-6.5.2-py3-none-any.whl", hash = "sha256:91757653bd9c550eeea2fa2404dba6b85d1e366d54804c340b2c874e5a7eb4a4", size = 9895973, upload-time = "2026-01-14T21:26:47.135Z" }, -] - -[[package]] -name = "ply" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130, upload-time = "2018-02-15T19:01:31.097Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567, upload-time = "2018-02-15T19:01:27.172Z" }, -] - -[[package]] -name = "portalocker" -version = "3.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pywin32", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" }, -] - -[[package]] -name = "posthog" -version = "7.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "backoff" }, - { name = "distro" }, - { name = "python-dateutil" }, - { name = "requests" }, - { name = "six" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/3b/866af11cb12e9d35feffcd480d4ebf31f87b2164926b9c670cbdafabc814/posthog-7.5.1.tar.gz", hash = "sha256:d8a8165b3d47465023ea2f919982a34890e2dda76402ec47d6c68424b2534a55", size = 145244, upload-time = "2026-01-08T21:18:39.266Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/03/ba011712ce9d07fe87dcfb72474c388d960e6d0c4f2262d2ae11fd27f0c5/posthog-7.5.1-py3-none-any.whl", hash = "sha256:fd3431ce32c9bbfb1e3775e3633c32ee589c052b0054fafe5ed9e4b17c1969d3", size = 167555, upload-time = "2026-01-08T21:18:37.437Z" }, -] - -[[package]] -name = "powerfx" -version = "0.0.34" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "python_full_version < '3.14'" }, - { name = "pythonnet", marker = "python_full_version < '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555, upload-time = "2025-12-22T15:50:59.682Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/96/0f8a1f86485b3ec0315e3e8403326884a0334b3dcd699df2482669cca4be/powerfx-0.0.34-py3-none-any.whl", hash = "sha256:f2dc1c42ba8bfa4c72a7fcff2a00755b95394547388ca0b3e36579c49ee7ed75", size = 3483089, upload-time = "2025-12-22T15:50:57.536Z" }, -] - -[[package]] -name = "propcache" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, -] - -[[package]] -name = "proto-plus" -version = "1.27.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/01/89/9cbe2f4bba860e149108b683bc2efec21f14d5f7ed6e25562ad86acbc373/proto_plus-1.27.0.tar.gz", hash = "sha256:873af56dd0d7e91836aee871e5799e1c6f1bda86ac9a983e0bb9f0c266a568c4", size = 56158, upload-time = "2025-12-16T13:46:25.729Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl", hash = "sha256:1baa7f81cf0f8acb8bc1f6d085008ba4171eaf669629d1b6d1673b21ed1c0a82", size = 50205, upload-time = "2025-12-16T13:46:24.76Z" }, -] - -[[package]] -name = "protobuf" -version = "5.29.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/29/d09e70352e4e88c9c7a198d5645d7277811448d76c23b00345670f7c8a38/protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84", size = 425226, upload-time = "2025-05-28T23:51:59.82Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/11/6e40e9fc5bba02988a214c07cf324595789ca7820160bfd1f8be96e48539/protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079", size = 422963, upload-time = "2025-05-28T23:51:41.204Z" }, - { url = "https://files.pythonhosted.org/packages/81/7f/73cefb093e1a2a7c3ffd839e6f9fcafb7a427d300c7f8aef9c64405d8ac6/protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc", size = 434818, upload-time = "2025-05-28T23:51:44.297Z" }, - { url = "https://files.pythonhosted.org/packages/dd/73/10e1661c21f139f2c6ad9b23040ff36fee624310dc28fba20d33fdae124c/protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671", size = 418091, upload-time = "2025-05-28T23:51:45.907Z" }, - { url = "https://files.pythonhosted.org/packages/6c/04/98f6f8cf5b07ab1294c13f34b4e69b3722bb609c5b701d6c169828f9f8aa/protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015", size = 319824, upload-time = "2025-05-28T23:51:47.545Z" }, - { url = "https://files.pythonhosted.org/packages/85/e4/07c80521879c2d15f321465ac24c70efe2381378c00bf5e56a0f4fbac8cd/protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61", size = 319942, upload-time = "2025-05-28T23:51:49.11Z" }, - { url = "https://files.pythonhosted.org/packages/7e/cc/7e77861000a0691aeea8f4566e5d3aa716f2b1dece4a24439437e41d3d25/protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5", size = 172823, upload-time = "2025-05-28T23:51:58.157Z" }, -] - -[[package]] -name = "psutil" -version = "7.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/cb/09e5184fb5fc0358d110fc3ca7f6b1d033800734d34cac10f4136cfac10e/psutil-7.2.1.tar.gz", hash = "sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3", size = 490253, upload-time = "2025-12-29T08:26:00.169Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/8e/f0c242053a368c2aa89584ecd1b054a18683f13d6e5a318fc9ec36582c94/psutil-7.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d", size = 129624, upload-time = "2025-12-29T08:26:04.255Z" }, - { url = "https://files.pythonhosted.org/packages/26/97/a58a4968f8990617decee234258a2b4fc7cd9e35668387646c1963e69f26/psutil-7.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49", size = 130132, upload-time = "2025-12-29T08:26:06.228Z" }, - { url = "https://files.pythonhosted.org/packages/db/6d/ed44901e830739af5f72a85fa7ec5ff1edea7f81bfbf4875e409007149bd/psutil-7.2.1-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc", size = 180612, upload-time = "2025-12-29T08:26:08.276Z" }, - { url = "https://files.pythonhosted.org/packages/c7/65/b628f8459bca4efbfae50d4bf3feaab803de9a160b9d5f3bd9295a33f0c2/psutil-7.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf", size = 183201, upload-time = "2025-12-29T08:26:10.622Z" }, - { url = "https://files.pythonhosted.org/packages/fb/23/851cadc9764edcc18f0effe7d0bf69f727d4cf2442deb4a9f78d4e4f30f2/psutil-7.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f", size = 139081, upload-time = "2025-12-29T08:26:12.483Z" }, - { url = "https://files.pythonhosted.org/packages/59/82/d63e8494ec5758029f31c6cb06d7d161175d8281e91d011a4a441c8a43b5/psutil-7.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672", size = 134767, upload-time = "2025-12-29T08:26:14.528Z" }, - { url = "https://files.pythonhosted.org/packages/05/c2/5fb764bd61e40e1fe756a44bd4c21827228394c17414ade348e28f83cd79/psutil-7.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679", size = 129716, upload-time = "2025-12-29T08:26:16.017Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d2/935039c20e06f615d9ca6ca0ab756cf8408a19d298ffaa08666bc18dc805/psutil-7.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f", size = 130133, upload-time = "2025-12-29T08:26:18.009Z" }, - { url = "https://files.pythonhosted.org/packages/77/69/19f1eb0e01d24c2b3eacbc2f78d3b5add8a89bf0bb69465bc8d563cc33de/psutil-7.2.1-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129", size = 181518, upload-time = "2025-12-29T08:26:20.241Z" }, - { url = "https://files.pythonhosted.org/packages/e1/6d/7e18b1b4fa13ad370787626c95887b027656ad4829c156bb6569d02f3262/psutil-7.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a", size = 184348, upload-time = "2025-12-29T08:26:22.215Z" }, - { url = "https://files.pythonhosted.org/packages/98/60/1672114392dd879586d60dd97896325df47d9a130ac7401318005aab28ec/psutil-7.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79", size = 140400, upload-time = "2025-12-29T08:26:23.993Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7b/d0e9d4513c46e46897b46bcfc410d51fc65735837ea57a25170f298326e6/psutil-7.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266", size = 135430, upload-time = "2025-12-29T08:26:25.999Z" }, - { url = "https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42", size = 128137, upload-time = "2025-12-29T08:26:27.759Z" }, - { url = "https://files.pythonhosted.org/packages/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1", size = 128947, upload-time = "2025-12-29T08:26:29.548Z" }, - { url = "https://files.pythonhosted.org/packages/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8", size = 154694, upload-time = "2025-12-29T08:26:32.147Z" }, - { url = "https://files.pythonhosted.org/packages/06/e4/b751cdf839c011a9714a783f120e6a86b7494eb70044d7d81a25a5cd295f/psutil-7.2.1-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6", size = 156136, upload-time = "2025-12-29T08:26:34.079Z" }, - { url = "https://files.pythonhosted.org/packages/44/ad/bbf6595a8134ee1e94a4487af3f132cef7fce43aef4a93b49912a48c3af7/psutil-7.2.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8", size = 148108, upload-time = "2025-12-29T08:26:36.225Z" }, - { url = "https://files.pythonhosted.org/packages/1c/15/dd6fd869753ce82ff64dcbc18356093471a5a5adf4f77ed1f805d473d859/psutil-7.2.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67", size = 147402, upload-time = "2025-12-29T08:26:39.21Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17", size = 136938, upload-time = "2025-12-29T08:26:41.036Z" }, - { url = "https://files.pythonhosted.org/packages/3e/73/2ce007f4198c80fcf2cb24c169884f833fe93fbc03d55d302627b094ee91/psutil-7.2.1-cp37-abi3-win_arm64.whl", hash = "sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442", size = 133836, upload-time = "2025-12-29T08:26:43.086Z" }, -] - -[[package]] -name = "pyarrow" -version = "22.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/d6/d0fac16a2963002fc22c8fa75180a838737203d558f0ed3b564c4a54eef5/pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a", size = 34204629, upload-time = "2025-10-24T10:06:20.274Z" }, - { url = "https://files.pythonhosted.org/packages/c6/9c/1d6357347fbae062ad3f17082f9ebc29cc733321e892c0d2085f42a2212b/pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901", size = 35985783, upload-time = "2025-10-24T10:06:27.301Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c0/782344c2ce58afbea010150df07e3a2f5fdad299cd631697ae7bd3bac6e3/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691", size = 45020999, upload-time = "2025-10-24T10:06:35.387Z" }, - { url = "https://files.pythonhosted.org/packages/1b/8b/5362443737a5307a7b67c1017c42cd104213189b4970bf607e05faf9c525/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e0a15757fccb38c410947df156f9749ae4a3c89b2393741a50521f39a8cf202a", size = 47724601, upload-time = "2025-10-24T10:06:43.551Z" }, - { url = "https://files.pythonhosted.org/packages/69/4d/76e567a4fc2e190ee6072967cb4672b7d9249ac59ae65af2d7e3047afa3b/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cedb9dd9358e4ea1d9bce3665ce0797f6adf97ff142c8e25b46ba9cdd508e9b6", size = 48001050, upload-time = "2025-10-24T10:06:52.284Z" }, - { url = "https://files.pythonhosted.org/packages/01/5e/5653f0535d2a1aef8223cee9d92944cb6bccfee5cf1cd3f462d7cb022790/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:252be4a05f9d9185bb8c18e83764ebcfea7185076c07a7a662253af3a8c07941", size = 50307877, upload-time = "2025-10-24T10:07:02.405Z" }, - { url = "https://files.pythonhosted.org/packages/2d/f8/1d0bd75bf9328a3b826e24a16e5517cd7f9fbf8d34a3184a4566ef5a7f29/pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145", size = 27977099, upload-time = "2025-10-24T10:08:07.259Z" }, - { url = "https://files.pythonhosted.org/packages/90/81/db56870c997805bf2b0f6eeeb2d68458bf4654652dccdcf1bf7a42d80903/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f7fe3dbe871294ba70d789be16b6e7e52b418311e166e0e3cba9522f0f437fb1", size = 34336685, upload-time = "2025-10-24T10:07:11.47Z" }, - { url = "https://files.pythonhosted.org/packages/1c/98/0727947f199aba8a120f47dfc229eeb05df15bcd7a6f1b669e9f882afc58/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ba95112d15fd4f1105fb2402c4eab9068f0554435e9b7085924bcfaac2cc306f", size = 36032158, upload-time = "2025-10-24T10:07:18.626Z" }, - { url = "https://files.pythonhosted.org/packages/96/b4/9babdef9c01720a0785945c7cf550e4acd0ebcd7bdd2e6f0aa7981fa85e2/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c064e28361c05d72eed8e744c9605cbd6d2bb7481a511c74071fd9b24bc65d7d", size = 44892060, upload-time = "2025-10-24T10:07:26.002Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ca/2f8804edd6279f78a37062d813de3f16f29183874447ef6d1aadbb4efa0f/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6f9762274496c244d951c819348afbcf212714902742225f649cf02823a6a10f", size = 47504395, upload-time = "2025-10-24T10:07:34.09Z" }, - { url = "https://files.pythonhosted.org/packages/b9/f0/77aa5198fd3943682b2e4faaf179a674f0edea0d55d326d83cb2277d9363/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746", size = 48066216, upload-time = "2025-10-24T10:07:43.528Z" }, - { url = "https://files.pythonhosted.org/packages/79/87/a1937b6e78b2aff18b706d738c9e46ade5bfcf11b294e39c87706a0089ac/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95", size = 50288552, upload-time = "2025-10-24T10:07:53.519Z" }, - { url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504, upload-time = "2025-10-24T10:08:00.932Z" }, - { url = "https://files.pythonhosted.org/packages/bd/b0/0fa4d28a8edb42b0a7144edd20befd04173ac79819547216f8a9f36f9e50/pyarrow-22.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9bddc2cade6561f6820d4cd73f99a0243532ad506bc510a75a5a65a522b2d74d", size = 34224062, upload-time = "2025-10-24T10:08:14.101Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a8/7a719076b3c1be0acef56a07220c586f25cd24de0e3f3102b438d18ae5df/pyarrow-22.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e70ff90c64419709d38c8932ea9fe1cc98415c4f87ea8da81719e43f02534bc9", size = 35990057, upload-time = "2025-10-24T10:08:21.842Z" }, - { url = "https://files.pythonhosted.org/packages/89/3c/359ed54c93b47fb6fe30ed16cdf50e3f0e8b9ccfb11b86218c3619ae50a8/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:92843c305330aa94a36e706c16209cd4df274693e777ca47112617db7d0ef3d7", size = 45068002, upload-time = "2025-10-24T10:08:29.034Z" }, - { url = "https://files.pythonhosted.org/packages/55/fc/4945896cc8638536ee787a3bd6ce7cec8ec9acf452d78ec39ab328efa0a1/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6dda1ddac033d27421c20d7a7943eec60be44e0db4e079f33cc5af3b8280ccde", size = 47737765, upload-time = "2025-10-24T10:08:38.559Z" }, - { url = "https://files.pythonhosted.org/packages/cd/5e/7cb7edeb2abfaa1f79b5d5eb89432356155c8426f75d3753cbcb9592c0fd/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:84378110dd9a6c06323b41b56e129c504d157d1a983ce8f5443761eb5256bafc", size = 48048139, upload-time = "2025-10-24T10:08:46.784Z" }, - { url = "https://files.pythonhosted.org/packages/88/c6/546baa7c48185f5e9d6e59277c4b19f30f48c94d9dd938c2a80d4d6b067c/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:854794239111d2b88b40b6ef92aa478024d1e5074f364033e73e21e3f76b25e0", size = 50314244, upload-time = "2025-10-24T10:08:55.771Z" }, - { url = "https://files.pythonhosted.org/packages/3c/79/755ff2d145aafec8d347bf18f95e4e81c00127f06d080135dfc86aea417c/pyarrow-22.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:b883fe6fd85adad7932b3271c38ac289c65b7337c2c132e9569f9d3940620730", size = 28757501, upload-time = "2025-10-24T10:09:59.891Z" }, - { url = "https://files.pythonhosted.org/packages/0e/d2/237d75ac28ced3147912954e3c1a174df43a95f4f88e467809118a8165e0/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a820d8ae11facf32585507c11f04e3f38343c1e784c9b5a8b1da5c930547fe2", size = 34355506, upload-time = "2025-10-24T10:09:02.953Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/733dfffe6d3069740f98e57ff81007809067d68626c5faef293434d11bd6/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:c6ec3675d98915bf1ec8b3c7986422682f7232ea76cad276f4c8abd5b7319b70", size = 36047312, upload-time = "2025-10-24T10:09:10.334Z" }, - { url = "https://files.pythonhosted.org/packages/7c/2b/29d6e3782dc1f299727462c1543af357a0f2c1d3c160ce199950d9ca51eb/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3e739edd001b04f654b166204fc7a9de896cf6007eaff33409ee9e50ceaff754", size = 45081609, upload-time = "2025-10-24T10:09:18.61Z" }, - { url = "https://files.pythonhosted.org/packages/8d/42/aa9355ecc05997915af1b7b947a7f66c02dcaa927f3203b87871c114ba10/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7388ac685cab5b279a41dfe0a6ccd99e4dbf322edfb63e02fc0443bf24134e91", size = 47703663, upload-time = "2025-10-24T10:09:27.369Z" }, - { url = "https://files.pythonhosted.org/packages/ee/62/45abedde480168e83a1de005b7b7043fd553321c1e8c5a9a114425f64842/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f633074f36dbc33d5c05b5dc75371e5660f1dbf9c8b1d95669def05e5425989c", size = 48066543, upload-time = "2025-10-24T10:09:34.908Z" }, - { url = "https://files.pythonhosted.org/packages/84/e9/7878940a5b072e4f3bf998770acafeae13b267f9893af5f6d4ab3904b67e/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4c19236ae2402a8663a2c8f21f1870a03cc57f0bef7e4b6eb3238cc82944de80", size = 50288838, upload-time = "2025-10-24T10:09:44.394Z" }, - { url = "https://files.pythonhosted.org/packages/7b/03/f335d6c52b4a4761bcc83499789a1e2e16d9d201a58c327a9b5cc9a41bd9/pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae", size = 29185594, upload-time = "2025-10-24T10:09:53.111Z" }, -] - -[[package]] -name = "pyasn1" -version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, -] - -[[package]] -name = "pyasn1-modules" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, -] - -[[package]] -name = "pycparser" -version = "2.23" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, -] - -[[package]] -name = "pydantic" -version = "2.12.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.41.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, -] - -[[package]] -name = "pydantic-settings" -version = "2.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, -] - -[[package]] -name = "pydeck" -version = "0.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/40e14e196864a0f61a92abb14d09b3d3da98f94ccb03b49cf51688140dab/pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605", size = 3832240, upload-time = "2024-05-10T15:36:21.153Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038", size = 6900403, upload-time = "2024-05-10T15:36:17.36Z" }, -] - -[[package]] -name = "pydyf" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/ee/fb410c5c854b6a081a49077912a9765aeffd8e07cbb0663cfda310b01fb4/pydyf-0.12.1.tar.gz", hash = "sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095", size = 17716, upload-time = "2025-12-02T14:52:14.244Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/11/47efe2f66ba848a107adfd490b508f5c0cedc82127950553dca44d29e6c4/pydyf-0.12.1-py3-none-any.whl", hash = "sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc", size = 8028, upload-time = "2025-12-02T14:52:12.938Z" }, -] - -[[package]] -name = "pyjwt" -version = "2.10.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, -] - -[package.optional-dependencies] -crypto = [ - { name = "cryptography" }, -] - -[[package]] -name = "pyphen" -version = "0.17.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/56/e4d7e1bd70d997713649c5ce530b2d15a5fc2245a74ca820fc2d51d89d4d/pyphen-0.17.2.tar.gz", hash = "sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3", size = 2079470, upload-time = "2025-01-20T13:18:36.296Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/1f/c2142d2edf833a90728e5cdeb10bdbdc094dde8dbac078cee0cf33f5e11b/pyphen-0.17.2-py3-none-any.whl", hash = "sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd", size = 2079358, upload-time = "2025-01-20T13:18:29.629Z" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "python-docx" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "lxml" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" }, -] - -[[package]] -name = "python-dotenv" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, -] - -[[package]] -name = "python-multipart" -version = "0.0.21" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, -] - -[[package]] -name = "python-ulid" -version = "3.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/40/7e/0d6c82b5ccc71e7c833aed43d9e8468e1f2ff0be1b3f657a6fcafbb8433d/python_ulid-3.1.0.tar.gz", hash = "sha256:ff0410a598bc5f6b01b602851a3296ede6f91389f913a5d5f8c496003836f636", size = 93175, upload-time = "2025-08-18T16:09:26.305Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/a0/4ed6632b70a52de845df056654162acdebaf97c20e3212c559ac43e7216e/python_ulid-3.1.0-py3-none-any.whl", hash = "sha256:e2cdc979c8c877029b4b7a38a6fba3bc4578e4f109a308419ff4d3ccf0a46619", size = 11577, upload-time = "2025-08-18T16:09:25.047Z" }, -] - -[[package]] -name = "pythonnet" -version = "3.0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "clr-loader", marker = "python_full_version < '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/f1/bfb6811df4745f92f14c47a29e50e89a36b1533130fcc56452d4660bd2d6/pythonnet-3.0.5-py3-none-any.whl", hash = "sha256:f6702d694d5d5b163c9f3f5cc34e0bed8d6857150237fae411fefb883a656d20", size = 297506, upload-time = "2024-12-13T08:30:40.661Z" }, -] - -[[package]] -name = "pytz" -version = "2025.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, -] - -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - -[[package]] -name = "qdrant-client" -version = "1.16.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "grpcio" }, - { name = "httpx", extra = ["http2"] }, - { name = "numpy" }, - { name = "portalocker" }, - { name = "protobuf" }, - { name = "pydantic" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ca/7d/3cd10e26ae97b35cf856ca1dc67576e42414ae39502c51165bb36bb1dff8/qdrant_client-1.16.2.tar.gz", hash = "sha256:ca4ef5f9be7b5eadeec89a085d96d5c723585a391eb8b2be8192919ab63185f0", size = 331112, upload-time = "2025-12-12T10:58:30.866Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/13/8ce16f808297e16968269de44a14f4fef19b64d9766be1d6ba5ba78b579d/qdrant_client-1.16.2-py3-none-any.whl", hash = "sha256:442c7ef32ae0f005e88b5d3c0783c63d4912b97ae756eb5e052523be682f17d3", size = 377186, upload-time = "2025-12-12T10:58:29.282Z" }, -] - -[[package]] -name = "redis" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, -] - -[[package]] -name = "redisvl" -version = "0.13.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonpath-ng" }, - { name = "ml-dtypes" }, - { name = "numpy" }, - { name = "pydantic" }, - { name = "python-ulid" }, - { name = "pyyaml" }, - { name = "redis" }, - { name = "tenacity" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/81/d6/8f3235b272e3a2370698d7524aad2dec15f53c5be5d6726ba41056844f69/redisvl-0.13.2.tar.gz", hash = "sha256:f34c4350922ac469c45d90b5db65c49950e6aa8706331931b000f631ff9a0f4a", size = 737736, upload-time = "2025-12-19T09:22:07.787Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/93/81ea5c45637ce7fe2fdaf214d5e1b91afe96a472edeb9b659e24d3710dfb/redisvl-0.13.2-py3-none-any.whl", hash = "sha256:dd998c6acc54f13526d464ad6b6e6f0c4cf6985fb2c7a1655bdf8ed8e57a4c01", size = 192760, upload-time = "2025-12-19T09:22:06.301Z" }, -] - -[[package]] -name = "referencing" -version = "0.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, -] - -[[package]] -name = "requests" -version = "2.32.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, -] - -[[package]] -name = "requests-oauthlib" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "oauthlib" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, -] - -[[package]] -name = "rfp-analyzer" -version = "0.1.0" -source = { virtual = "." } -dependencies = [ - { name = "agent-framework" }, - { name = "azure-ai-documentintelligence" }, - { name = "azure-core-tracing-opentelemetry" }, - { name = "azure-identity" }, - { name = "azure-monitor-opentelemetry" }, - { name = "openai" }, - { name = "opentelemetry-sdk" }, - { name = "plotly" }, - { name = "pydantic" }, - { name = "python-docx" }, - { name = "python-dotenv" }, - { name = "requests" }, - { name = "streamlit" }, -] - -[package.optional-dependencies] -pdf = [ - { name = "markdown" }, - { name = "weasyprint" }, -] - -[package.dev-dependencies] -dev = [ - { name = "watchdog" }, -] - -[package.metadata] -requires-dist = [ - { name = "agent-framework" }, - { name = "azure-ai-documentintelligence", specifier = ">=1.0.2" }, - { name = "azure-core-tracing-opentelemetry", specifier = ">=1.0.0b12" }, - { name = "azure-identity", specifier = ">=1.19.0" }, - { name = "azure-monitor-opentelemetry", specifier = ">=1.8.3" }, - { name = "markdown", marker = "extra == 'pdf'", specifier = ">=3.7" }, - { name = "openai", specifier = ">=2.15.0" }, - { name = "opentelemetry-sdk", specifier = ">=1.39.0" }, - { name = "plotly", specifier = ">=6.5.2" }, - { name = "pydantic", specifier = ">=2.12.5" }, - { name = "python-docx", specifier = ">=1.1.0" }, - { name = "python-dotenv", specifier = ">=1.2.1" }, - { name = "requests", specifier = ">=2.32.5" }, - { name = "streamlit", specifier = ">=1.52.2" }, - { name = "weasyprint", marker = "extra == 'pdf'", specifier = ">=62.0" }, -] -provides-extras = ["pdf"] - -[package.metadata.requires-dev] -dev = [{ name = "watchdog", specifier = ">=6.0.0" }] - -[[package]] -name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, -] - -[[package]] -name = "rsa" -version = "4.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "smmap" -version = "5.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - -[[package]] -name = "sqlalchemy" -version = "2.0.45" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" }, - { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" }, - { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" }, - { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" }, - { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" }, - { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" }, - { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" }, - { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" }, - { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" }, - { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" }, - { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" }, - { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" }, - { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, -] - -[[package]] -name = "sse-starlette" -version = "3.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "starlette" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/34/f5df66cb383efdbf4f2db23cabb27f51b1dcb737efaf8a558f6f1d195134/sse_starlette-3.1.2.tar.gz", hash = "sha256:55eff034207a83a0eb86de9a68099bd0157838f0b8b999a1b742005c71e33618", size = 26303, upload-time = "2025-12-31T08:02:20.023Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/95/8c4b76eec9ae574474e5d2997557cebf764bcd3586458956c30631ae08f4/sse_starlette-3.1.2-py3-none-any.whl", hash = "sha256:cd800dd349f4521b317b9391d3796fa97b71748a4da9b9e00aafab32dda375c8", size = 12484, upload-time = "2025-12-31T08:02:18.894Z" }, -] - -[[package]] -name = "starlette" -version = "0.50.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, -] - -[[package]] -name = "streamlit" -version = "1.52.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "altair" }, - { name = "blinker" }, - { name = "cachetools" }, - { name = "click" }, - { name = "gitpython" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pandas" }, - { name = "pillow" }, - { name = "protobuf" }, - { name = "pyarrow" }, - { name = "pydeck" }, - { name = "requests" }, - { name = "tenacity" }, - { name = "toml" }, - { name = "tornado" }, - { name = "typing-extensions" }, - { name = "watchdog", marker = "sys_platform != 'darwin'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/20/434aaceccc6e1912671d869926103051330437adba72d538d787a07727ef/streamlit-1.52.2.tar.gz", hash = "sha256:64a4dda8bc5cdd37bfd490e93bb53da35aaef946fcfc283a7980dacdf165108b", size = 8584178, upload-time = "2025-12-17T17:07:59.642Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/95/6b7873f0267973ebd55ba9cd33a690b35a116f2779901ef6185a0e21864d/streamlit-1.52.2-py3-none-any.whl", hash = "sha256:a16bb4fbc9781e173ce9dfbd8ffb189c174f148f9ca4fb8fa56423e84e193fc8", size = 9025937, upload-time = "2025-12-17T17:07:57.67Z" }, -] - -[[package]] -name = "tenacity" -version = "9.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, -] - -[[package]] -name = "tinycss2" -version = "1.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "webencodings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, -] - -[[package]] -name = "tinyhtml5" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "webencodings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/03/6111ed99e9bf7dfa1c30baeef0e0fb7e0bd387bd07f8e5b270776fe1de3f/tinyhtml5-2.0.0.tar.gz", hash = "sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc", size = 179507, upload-time = "2024-10-29T15:37:14.078Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/de/27c57899297163a4a84104d5cec0af3b1ac5faf62f44667e506373c6b8ce/tinyhtml5-2.0.0-py3-none-any.whl", hash = "sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e", size = 39793, upload-time = "2024-10-29T15:37:11.743Z" }, -] - -[[package]] -name = "toml" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, -] - -[[package]] -name = "tornado" -version = "6.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" }, - { url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" }, - { url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" }, - { url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" }, - { url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" }, - { url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" }, - { url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" }, - { url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" }, - { url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" }, - { url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" }, -] - -[[package]] -name = "tqdm" -version = "4.67.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, -] - -[[package]] -name = "types-requests" -version = "2.32.4.20260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, -] - -[[package]] -name = "tzdata" -version = "2025.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, -] - -[[package]] -name = "urllib3" -version = "2.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, -] - -[[package]] -name = "uvicorn" -version = "0.40.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, -] - -[package.optional-dependencies] -standard = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "httptools" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, - { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, - { name = "watchfiles" }, - { name = "websockets" }, -] - -[[package]] -name = "uvloop" -version = "0.22.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, - { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, - { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, - { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, - { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, - { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, - { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, - { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, - { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, - { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, - { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, - { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, - { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, - { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, - { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, - { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, -] - -[[package]] -name = "watchdog" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, -] - -[[package]] -name = "watchfiles" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, - { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, - { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, - { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, - { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, - { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, - { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, - { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, - { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, - { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, - { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, - { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, - { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, - { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, - { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, - { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, - { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, - { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, - { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, - { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, - { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, - { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, - { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, - { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, - { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, - { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, - { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, - { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, -] - -[[package]] -name = "weasyprint" -version = "67.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, - { name = "cssselect2" }, - { name = "fonttools", extra = ["woff"] }, - { name = "pillow" }, - { name = "pydyf" }, - { name = "pyphen" }, - { name = "tinycss2" }, - { name = "tinyhtml5" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/bc/79a65b3a406cb62a1982fec8b49134b25a3b31abb094ca493c9fddff5492/weasyprint-67.0.tar.gz", hash = "sha256:fdfbccf700e8086c8fd1607ec42e25d4b584512c29af2d9913587a4e448dead4", size = 1534152, upload-time = "2025-12-02T16:11:36.972Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/3a/a225e214ae2accd8781e4d22e9397bd51290c631ea0943d3a0a1840bc667/weasyprint-67.0-py3-none-any.whl", hash = "sha256:abc2f40872ea01c29c11f7799dafc4b23c078335bf7777f72a8affeb36e1d201", size = 316309, upload-time = "2025-12-02T16:11:35.402Z" }, -] - -[[package]] -name = "webencodings" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, -] - -[[package]] -name = "websockets" -version = "16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, - { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, - { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, - { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, -] - -[[package]] -name = "werkzeug" -version = "3.1.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, -] - -[[package]] -name = "wrapt" -version = "1.17.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, - { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, - { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, - { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, - { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, - { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, - { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, - { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, - { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, - { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, - { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, - { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, - { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, - { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, - { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, - { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, - { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, - { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, - { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, - { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, - { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, -] - -[[package]] -name = "yarl" -version = "1.22.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, - { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, - { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, - { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, - { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, - { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, - { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, - { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, - { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, - { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, - { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, - { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, - { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, - { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, - { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, - { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, - { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, - { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, - { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, - { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, - { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, - { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, - { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, - { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, - { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, - { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, - { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, - { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, - { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, - { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, - { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, - { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, - { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, - { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, - { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, - { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, - { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, - { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, - { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, - { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, - { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, - { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, - { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, - { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, - { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, -] - -[[package]] -name = "zipp" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, -] - -[[package]] -name = "zopfli" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/4c/efa0760686d4cc69e68a8f284d3c6c5884722c50f810af0e277fb7d61621/zopfli-0.4.0.tar.gz", hash = "sha256:a8ee992b2549e090cd3f0178bf606dd41a29e0613a04cdf5054224662c72dce6", size = 176720, upload-time = "2025-11-07T17:00:59.507Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/62/ec5cb67ee379c6a4f296f1277b971ff8c26460bf8775f027f82c519a0a72/zopfli-0.4.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d1b98ad47c434ef213444a03ef2f826eeec100144d64f6a57504b9893d3931ce", size = 287433, upload-time = "2025-11-07T17:00:45.662Z" }, - { url = "https://files.pythonhosted.org/packages/5a/9e/8f81e69bd771014a488c4c64476b6e6faab91b2c913d0f81eca7e06401eb/zopfli-0.4.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:18b5f1570f64d4988482e4466f10ef5f2a30f687c19ad62a64560f2152dc89eb", size = 847135, upload-time = "2025-11-07T17:00:47.483Z" }, - { url = "https://files.pythonhosted.org/packages/24/84/6e60eeaaa1c1eae7b4805f1c528f3e8ae62cef323ec1e52347a11031e3ba/zopfli-0.4.0-cp310-abi3-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b72a010d205d00b2855acc2302772067362f9ab5a012e3550662aec60d28e6b3", size = 831606, upload-time = "2025-11-07T17:00:48.576Z" }, - { url = "https://files.pythonhosted.org/packages/6d/aa/a4d5de7ed8e809953cb5e8992bddc40f38461ec5a44abfb010953875adfc/zopfli-0.4.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c3ba02a9a6ca90481d2b2f68bab038b310d63a1e3b5ae305e95a6599787ed941", size = 1789376, upload-time = "2025-11-07T17:00:49.63Z" }, - { url = "https://files.pythonhosted.org/packages/39/95/4d1e943fbc44157f58b623625686d0b970f2fda269e721fbf9546b93f6cc/zopfli-0.4.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7d66337be6d5613dec55213e9ac28f378c41e2cc04fbad4a10748e4df774ca85", size = 1879013, upload-time = "2025-11-07T17:00:50.751Z" }, - { url = "https://files.pythonhosted.org/packages/95/db/4f2eebf73c0e2df293a366a1d176cd315a74ce0b00f83826a7ba9ddd1ab3/zopfli-0.4.0-cp310-abi3-win32.whl", hash = "sha256:03181d48e719fcb6cf8340189c61e8f9883d8bbbdf76bf5212a74457f7d083c1", size = 83655, upload-time = "2025-11-07T17:00:51.797Z" }, - { url = "https://files.pythonhosted.org/packages/24/f6/bd80c5278b1185dc41155c77bc61bfe1d817254a7f2115f66aa69a270b89/zopfli-0.4.0-cp310-abi3-win_amd64.whl", hash = "sha256:f94e4dd7d76b4fe9f5d9229372be20d7f786164eea5152d1af1c34298c3d5975", size = 100824, upload-time = "2025-11-07T17:00:52.658Z" }, -] diff --git a/azure.yaml b/azure.yaml index db4ca5b..0f66b51 100644 --- a/azure.yaml +++ b/azure.yaml @@ -2,11 +2,12 @@ name: rfp-analyzer services: rfp-analyzer: - project: app + project: app/RfpAnalyzer host: containerapp - language: python + language: dotnet docker: - path: Dockerfile + path: ../Dockerfile + context: ../ remoteBuild: true hooks: