diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml new file mode 100644 index 0000000..6bfb33e --- /dev/null +++ b/.github/workflows/dev.yml @@ -0,0 +1,70 @@ +name: flet-pkg - Dev + +on: + push: + branches: [ dev ] + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [ + "3.10", +# "3.11", + "3.13", +# "3.14" + ] + fail-fast: false + + steps: + - uses: actions/checkout@v4 + + - name: Set up UV and Python ${{ matrix.python-version }} + uses: astral-sh/setup-uv@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache UV dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/uv + key: uv-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('uv.lock') }} + restore-keys: | + uv-${{ runner.os }}-py${{ matrix.python-version }}- + uv-${{ runner.os }}- + + - name: Sync dependencies with UV + run: | + uv sync --dev --frozen --no-editable + + - name: Check code formatting with Ruff + run: | + uv tool run ruff format --check + + - name: Lint with Ruff + run: | + uv tool run ruff check + + - name: Check types with ty + run: | + uv tool run ty check + + - name: Run tests with coverage + run: | + uv run pytest -v -m "not network" --cov=flet_pkg --cov-report=term-missing --cov-report=xml + + - name: Upload coverage to Codecov + if: matrix.python-version == '3.13' + uses: codecov/codecov-action@v5 + with: + files: coverage.xml + flags: unittests + fail_ci_if_error: false + + - name: Build docs + if: matrix.python-version == '3.13' + run: | + uv sync --group docs --frozen --no-editable + NO_MKDOCS_2_WARNING=1 uv run --group docs mkdocs build --strict diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..ba8ba0f --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,55 @@ +name: Deploy Documentation + +on: + push: + branches: [ main ] + workflow_dispatch: + +permissions: + contents: read + id-token: write + pages: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up UV and Python + uses: astral-sh/setup-uv@v6 + with: + python-version: "3.13" + + - name: Cache UV dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/uv + key: uv-docs-${{ runner.os }}-${{ hashFiles('uv.lock') }} + restore-keys: | + uv-docs-${{ runner.os }}- + + - name: Install dependencies + run: | + uv sync --group docs --frozen --no-editable + + - name: Build documentation + run: | + NO_MKDOCS_2_WARNING=1 uv run --group docs mkdocs build --strict + + - name: Upload to GitHub Pages + uses: actions/upload-pages-artifact@v4 + with: + path: site + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..7c30872 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,105 @@ +name: flet-pkg - Main + +on: + pull_request: + branches: [ main ] + types: [opened, synchronize, reopened] + push: + branches: [ main ] + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [ + "3.10", +# "3.11", + "3.13", +# "3.14" + ] + fail-fast: false + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up UV and Python ${{ matrix.python-version }} + uses: astral-sh/setup-uv@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache UV dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/uv + key: uv-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('uv.lock') }} + restore-keys: | + uv-${{ runner.os }}-py${{ matrix.python-version }}- + uv-${{ runner.os }}- + + - name: Sync dependencies with UV + run: | + uv sync --dev --frozen --no-editable + + - name: Check code formatting with Ruff + run: | + uv tool run ruff format --check + + - name: Lint with Ruff + run: | + uv tool run ruff check + + - name: Check types with ty + run: | + uv tool run ty check + + - name: Run tests with coverage + run: | + uv run pytest -v -m "not network" --cov=flet_pkg --cov-report=term-missing --cov-report=xml + + - name: Upload coverage to Codecov + if: matrix.python-version == '3.13' + uses: codecov/codecov-action@v5 + with: + files: coverage.xml + flags: unittests + fail_ci_if_error: false + + - name: Build docs + if: matrix.python-version == '3.13' + run: | + uv sync --group docs --frozen --no-editable + NO_MKDOCS_2_WARNING=1 uv run --group docs mkdocs build --strict + + - name: Verify version was bumped + if: github.event_name == 'pull_request' + run: | + git fetch origin main + + # Skip version check if only docs/examples changed + changed=$(git diff --name-only origin/main...HEAD) + code_changed=$(echo "$changed" | grep -vE '^(docs/|examples/|README\.md|mkdocs\.yml|scripts/|\.github/)' || true) + + if [ -z "$code_changed" ]; then + echo "✅ Docs/examples-only PR — version bump not required" + exit 0 + fi + + current=$(grep -E '^version = ' pyproject.toml | cut -d'"' -f2) + + if [[ ! $current =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "❌ Version must follow semantic versioning (e.g., 0.2.0)" + exit 1 + fi + + base=$(git show origin/main:pyproject.toml | grep -E '^version = ' | cut -d'"' -f2) + + if [ "$current" = "$base" ]; then + echo "❌ Version not bumped: $current (same as main)" + exit 1 + fi + + echo "✅ Version bumped: $base -> $current" diff --git a/.gitignore b/.gitignore index 3c4e98a..17741ff 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.swp *.tmp *.lock +!uv.lock .DS_Store .atom/ .buildlog/ @@ -238,9 +239,5 @@ poetry.toml # LSP config files pyrightconfig.json -dart_wrappers/ -package/ -flet_pkg/python_wrappers/ - .claude tmp \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..dab7f8b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +## [0.1.0] - 2026-03-05 + +### Added + +- **CLI tool** (`flet-pkg create`) with interactive and non-interactive modes for scaffolding Flet extension packages. +- **Auto-detect extension type** — downloads the Flutter package and inspects Dart source to determine `service` or `ui_control` automatically. +- **Deterministic generation pipeline** — download, parse, analyze, generate, write — produces Python controls, Dart bridge code, type mappings, enum definitions, and event handlers from any Flutter package on pub.dev. +- **Service extensions** (`ft.Service`) with async method invocation via `invoke_method`. +- **UI Control extensions** (`ft.LayoutControl`) with constructor-to-property mapping. +- **Sub-module grouping** — related Dart classes (e.g. `OneSignalUser`, `OneSignalNotifications`) become separate Python modules. +- **Compound widget detection** — typed sub-widget constructor params become `ft.Control` sub-classes. +- **Stream event detection** — `Stream get onXxx` patterns become Python `ft.EventHandler` attributes. +- **Platform interface resolution** — downloads `*_platform_interface` packages to fill stub enum values and data class fields. +- **Type mapping** — comprehensive Dart-to-Python/Flet type mapping (`core/type_map.py`) including Flutter enums, generics, and nullable types. +- **Name conflict detection** — checks PyPI, GitHub, and Flet SDK monorepo before creating a package. +- **Verbose mode** (`-v`) with deterministic coverage score and Rich breakdown table per category (Methods, Events, Enums, Properties). +- **Debug console module** — optional `console.py` with `DebugConsole` widget and `setup_logging()` for development. +- **AI refinement** (optional, `flet-pkg[ai]`) — post-generation LLM-powered code improvement using the Architect/Editor pattern: + - Deterministic gap analysis comparing Dart API vs generated Python code. + - Architect agent reasons about how to fix each gap. + - Editor agent produces precise search/replace code edits. + - Validator checks Python syntax with retry loop. + - Supports Ollama (free, local), Anthropic, OpenAI, and Google providers. + - Diff review, spinners, and coverage comparison output. +- **MCP Server** (optional, `flet-pkg[mcp]`) — Model Context Protocol server exposing scaffolding and code generation tools to AI agents (Claude Desktop, Cursor, VS Code Copilot). +- **Documentation** — MkDocs Material site with API Reference (mkdocstrings), architecture guide, CLI reference, and MCP server docs. +- **Generated project structure** includes `pyproject.toml`, `README.md`, `mkdocs.yml`, tests, examples, and docs scaffolding. +- **Template engine** — Jinja2 templates for both `service` and `ui_control` types with `include_console` conditionals. +- **Python reserved word sanitization** — module names and method names that collide with Python keywords (e.g. `async`, `await`) are automatically renamed. +- **Dart control-flow filtering** — false method matches from `catch`/`finally`/`then` patterns are excluded from generation. +- **Test suite** — 383 tests covering models, type map, downloader, analyzer, generators, pipeline, scaffolder, CLI, validators, and coverage. +- **CI/CD** — GitHub Actions workflows for `dev` and `main` branches with ruff, ty, pytest, and mkdocs build. + +[0.1.0]: https://github.com/brunobrown/flet-pkg/releases/tag/v0.1.0 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cf92148 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Bruno Brown + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index db67715..116bc33 100644 --- a/README.md +++ b/README.md @@ -1 +1,689 @@ -# flet-pkg \ No newline at end of file +

Flet PKG

+ +**CLI tool that scaffolds Flet extension packages from Flutter packages, with auto-generated Python controls, Dart bridge code, type mappings, and event handlers.** + +![Python](https://img.shields.io/badge/python-3.10+-3776AB?logo=python&logoColor=white) +![Flet](https://img.shields.io/badge/flet-0.80.0+-00B4D8?logo=flet) +[![Main](https://github.com/brunobrown/flet-pkg/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/brunobrown/flet-pkg/actions/workflows/main.yml) +[![Dev](https://github.com/brunobrown/flet-pkg/actions/workflows/dev.yml/badge.svg?branch=dev)](https://github.com/brunobrown/flet-pkg/actions/workflows/dev.yml) +![Docs](https://img.shields.io/badge/https%3A%2F%2Fimg.shields.io%2Fbadge%2Fdocs-mkdocs-blue?label=docs) +[![Coverage](https://codecov.io/gh/brunobrown/flet-pkg/graph/badge.svg)](https://codecov.io/gh/brunobrown/flet-pkg) +![License](https://img.shields.io/badge/license-MIT-yellow) + +--- + +> **Important: this tool does not perform miracles.** 🙏😂 +> +> Every Flutter package on [pub.dev](https://pub.dev) has its own API surface, configuration requirements, and platform-specific behaviors. Some packages expose simple method calls; others require complex initialization flows, native platform setup (Android manifests, iOS plists, Gradle/CocoaPods configuration), or callback-based architectures that don't map cleanly to Python. +> +> It would be unrealistic to write a code generation algorithm that generalizes perfectly for every possible Flutter package. **flet-pkg** aims to accelerate the process by delivering significantly more than a blank skeleton — it downloads the real Dart source, parses the public API, and auto-generates Python controls, type mappings, enum definitions, event handlers, and Dart bridge code. For service packages, this typically covers ~95% of the API surface. +> +> However, **the developer is still responsible for**: +> - Understanding how the original Flutter/Dart package works +> - Reviewing the generated code for correctness +> - Handling package-specific configurations (native platform setup, initialization flows, etc.) +> - Adjusting or complementing the generated code for edge cases the pipeline could not cover +> +> Think of flet-pkg as a smart starting point, not a finished product. + +--- + +## Table of Contents + +- [Installation](#installation) +- [Quick Start](#quick-start) +- [How It Works](#how-it-works) +- [Usage](#usage) + - [Interactive Mode](#interactive-mode) + - [Non-Interactive Mode (CI/Scripting)](#non-interactive-mode-ciscripting) + - [Using a Local Flutter Package](#using-a-local-flutter-package) +- [Extension Types](#extension-types) +- [CLI Reference](#cli-reference) + - [flet-pkg --help](#flet-pkg---help) + - [flet-pkg create](#flet-pkg-create) + - [Package Options](#package-options) + - [Code Generation Options](#code-generation-options) + - [AI Refinement Options](#ai-refinement-options) + - [Output Options](#output-options) +- [Verbose Mode & Coverage Score](#verbose-mode--coverage-score) +- [AI Refinement](#ai-refinement) + - [Free (Ollama - Local)](#free-ollama---local) + - [Ollama Model Requirements](#ollama-model-requirements) + - [Cloud Providers](#cloud-providers) + - [How AI Refinement Works](#how-ai-refinement-works) +- [MCP Server](#mcp-server) + - [Installation](#mcp-installation) + - [Claude Desktop Configuration](#claude-desktop-configuration) + - [Available Tools](#available-tools) + - [Resources & Prompts](#resources--prompts) +- [Generated Project Structure](#generated-project-structure) +- [Name Conflict Detection](#name-conflict-detection) +- [Generation Pipeline](#generation-pipeline) +- [Coverage](#coverage) +- [Examples](#examples) +- [Development](#development) +- [Learn more](#learn-more) +- [Flet Community](#flet-community) +- [Support](#support) +- [Contributing](#contributing) + +--- + +## Buy Me a Coffee + +If you find this project useful, please consider supporting its development: + + +Buy Me a Coffee + + +--- + +## Installation + +```bash +# Using UV (recommended) +uv add flet-pkg + +# Using pip +pip install flet-pkg +``` + +To enable **AI refinement** (optional): + +```bash +# Using UV +uv add flet-pkg[ai] + +# Using pip +pip install flet-pkg[ai] +``` + +To enable the **MCP server** (optional — for AI agent integrations): + +```bash +# Using UV +uv add flet-pkg[mcp] + +# Using pip +pip install flet-pkg[mcp] +``` + +**Requirements:** Python 3.10+ + +--- + +## Quick Start + +```bash +# Interactive mode — prompts guide you through every step +flet-pkg create + +# One-liner for a service extension +flet-pkg create -t service -f shared_preferences + +# One-liner for a UI control extension +flet-pkg create -t ui_control -f shimmer + +# Auto-detect type + AI refinement (free, uses Ollama locally) +flet-pkg create -f onesignal_flutter --ai-refine + +# Verbose mode — detailed output + coverage breakdown table +flet-pkg create -t service -f shared_preferences -v +``` + +--- + +## How It Works + +flet-pkg automates the tedious parts of creating a Flet extension: + +1. **Downloads** the Flutter package source from [pub.dev](https://pub.dev) (or uses a local path) +2. **Parses** the Dart source files to extract the public API (classes, methods, constructors, enums, streams) +3. **Analyzes** the API to produce a generation plan (type mappings, event detection, sub-module grouping) +4. **Generates** Python controls (`ft.Service` / `ft.LayoutControl`), Dart bridge code, type definitions, and `__init__.py` exports +5. **Writes** everything into a ready-to-develop project with `pyproject.toml`, tests, examples, and docs scaffolding + +Optionally, an **AI refinement** step can analyze coverage gaps and auto-improve the generated code using LLM-powered edits. + +--- + +## Usage + +### Interactive Mode + +```bash +flet-pkg create +``` + +![flet_pkg](https://github.com/user-attachments/assets/dea17fa6-496e-4666-9127-c159e2954797) + +The CLI walks you through each step with prompts: + +1. **Extension type** — Auto-detect (recommended), Service, or UI Control +2. **Flutter package name** — The package name from pub.dev (e.g. `onesignal_flutter`) +3. **Extension name** — Your Flet extension project name (e.g. `flet-onesignal`) +4. **Python package name** — The importable package name (e.g. `flet_onesignal`) +5. **Control class name** — The Python class name (e.g. `OneSignal`) +6. **Description** — A short description for `pyproject.toml` +7. **Author** — Your name +8. **Debug console** — Whether to include a debug console module +9. **AI refinement** — Whether to run AI-powered code improvement (only shown if `pydantic-ai` is installed) + +### Non-Interactive Mode (CI/Scripting) + +Pass all options as flags to skip prompts entirely: + +```bash +flet-pkg create \ + --type service \ + --flutter-package shared_preferences \ + --output ./my-extensions +``` + +For a UI control with AI refinement: + +```bash +flet-pkg create \ + --type ui_control \ + --flutter-package shimmer \ + --ai-refine \ + --ai-provider ollama \ + --no-console +``` + +### Using a Local Flutter Package + +If you have the Flutter package source locally (useful for private packages or development): + +```bash +flet-pkg create \ + --type service \ + --flutter-package my_package \ + --local-package ./path/to/my_package +``` + +This skips the pub.dev download and uses your local files directly. + +--- + +## Extension Types + +| Type | Base Class | Description | Example Packages | +|------|-----------|-------------|------------------| +| **Auto-detect** | _(detected)_ | Downloads the package and inspects the Dart source to determine the type automatically. Falls back to a manual prompt if detection fails. | Any package | +| **Service** | `ft.Service` | Non-visual extensions that provide platform services — push notifications, storage, sensors, authentication. Methods are exposed as async Python calls via `invoke_method`. | `shared_preferences`, `onesignal_flutter`, `geolocator`, `local_auth`, `battery_plus` | +| **UI Control** | `ft.LayoutControl` | Visual widget extensions that render Flutter widgets on screen. Constructor parameters become Python properties mapped via `_get_control_name` and `_before_build_command`. | `shimmer`, `flutter_rating_bar`, `rive`, `video_player` | + +--- + +## CLI Reference + +### `flet-pkg --help` or `flet-pkg create --help` + +``` +Usage: flet-pkg [OPTIONS] COMMAND [ARGS]... + +Options: + --version Show version and exit. + --help Show this message and exit. + +Commands: + create Create a new Flet extension package. +``` + +### flet-pkg create + +``` +Usage: flet-pkg create [OPTIONS] +``` + +#### Package Options + +| Option | Short | Type | Description | +|--------|-------|------|-------------| +| `--type` | `-t` | TEXT | Extension type: `auto` (default), `service`, or `ui_control`. | +| `--flutter-package` | `-f` | TEXT | Flutter package name from [pub.dev](https://pub.dev). | +| `--output` | `-o` | PATH | Output directory for the generated project. Defaults to current directory. | +| `--local-package` | `-l` | PATH | Path to a local Flutter package source. Skips the pub.dev download. | + +#### Code Generation Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `--analyze / --no-analyze` | BOOL | `analyze` | Analyze the Flutter package and auto-generate Python controls, type mappings, and Dart bridge code. Use `--no-analyze` to generate only the skeleton structure without code analysis. | +| `--console / --no-console` | BOOL | _(prompted)_ | Include a debug console module (`console.py`) for development logging. | + +#### AI Refinement Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `--ai-refine / --no-ai-refine` | BOOL | _(prompted)_ | Run AI-powered refinement on generated code. Requires `uv add flet-pkg[ai]` or `pip install flet-pkg[ai]`. | +| `--ai-provider` | TEXT | `ollama` | AI provider: `ollama` (local, free), `anthropic` (Claude), `openai` (GPT), or `google` (Gemini). | +| `--ai-model` | TEXT | _(per provider)_ | Override the default model. Defaults: `ollama`=`qwen2.5-coder:14b`, `anthropic`=`claude-sonnet-4-6`, `openai`=`gpt-4.1-mini`, `google`=`gemini-2.5-flash`. See [Ollama Model Requirements](#ollama-model-requirements) for RAM sizing. | + +#### Output Options + +| Option | Short | Type | Default | Description | +|--------|-------|------|---------|-------------| +| `--verbose` | `-v` | BOOL | `False` | Show detailed analysis output and coverage breakdown table. Without this flag, only a one-line coverage score is shown. | + +--- + +## Verbose Mode & Coverage Score + +After code generation, flet-pkg runs a **deterministic gap analysis** that compares the Dart API against the generated Python code. A **coverage score** is always displayed: + +``` + Coverage: 95.4% (41/43 features mapped) +``` + +### Verbose Mode (`-v`) + +Pass `--verbose` or `-v` to see detailed output at every pipeline step: + +```bash +# Score only (default) +flet-pkg create -t service -f shared_preferences --no-ai-refine + +# Full verbose output with breakdown table +flet-pkg create -t service -f shared_preferences --no-ai-refine -v + +# Verbose + AI refinement details +flet-pkg create -t service -f shared_preferences --ai-refine -v +``` + +In verbose mode you'll see: + +- **Parse details** — every class and enum discovered in the Dart source +- **Analyze details** — each method, sub-module, event, and property mapped +- **Generate details** — file breakdown (Python vs Dart) with filenames +- **AI details** _(when `--ai-refine`)_ — gap report, architect suggestions, editor edits, validation results +- **Coverage breakdown table** — per-category Rich table: + +``` + Coverage Breakdown +┌────────────────┬──────────┬─────────┬─────────────────┐ +│ Category │ Dart API │ Mapped │ Coverage │ +├────────────────┼──────────┼─────────┼─────────────────┤ +│ Methods │ 25 │ 24 │ ████████░░ 96% │ +│ Events │ 5 │ 5 │ ██████████ 100% │ +│ Enums │ 8 │ 7 │ ███████░░░ 88% │ +├────────────────┼──────────┼─────────┼─────────────────┤ +│ Total │ 38 │ 36 │ █████████░ 95% │ +└────────────────┴──────────┴─────────┴─────────────────┘ +``` + +--- + +## AI Refinement + +The AI refinement is an **optional** post-generation step that uses an LLM to detect coverage gaps and auto-improve the generated code. It uses the **Architect/Editor pattern** (inspired by [Aider's research](https://aider.chat/2024/09/26/architect.html)) which separates reasoning from editing for better results. + +### Free (Ollama - Local) + +No API key needed. Runs entirely on your machine: + +```bash +# 1. Install Ollama (https://ollama.com) +curl -fsSL https://ollama.ai/install.sh | sh + +# 2. Pull the default coding model (see RAM requirements below) +ollama pull qwen2.5-coder:14b + +# 3. Install the AI dependency +uv add flet-pkg[ai] # or: pip install flet-pkg[ai] + +# 4. Create with AI refinement (Ollama is the default provider) +flet-pkg create -f shimmer --ai-refine +``` + +#### Ollama Model Requirements + +Choose the model size based on your available RAM. Larger models produce better results but require more memory: + +| Model | Size | Min RAM | GPU | Notes | +|-------|------|---------|-----|-------| +| `qwen2.5-coder:7b` | ~4.5 GB | 8 GB | Optional | Fast, but may fail on complex structured output | +| `qwen2.5-coder:14b` | ~9 GB | 16 GB | Optional | **Default — best balance of quality and speed** | +| `qwen2.5-coder:32b` | ~19 GB | 32 GB | Recommended | Best quality, but slow without dedicated GPU | + +> **Warning:** Running a model that exceeds your available RAM will cause severe system slowdown or freeze. If you have **no dedicated GPU** (NVIDIA/AMD), the model runs entirely on CPU+RAM. For machines with 24 GB RAM or less, use the **14b** model (default). +> +> To override the model: `flet-pkg create --ai-refine --ai-model qwen2.5-coder:7b` + +### Cloud Providers + +If you prefer a cloud-hosted LLM, set the appropriate API key: + +```bash +# Anthropic (Claude) +export ANTHROPIC_API_KEY=sk-ant-... +flet-pkg create -f shimmer --ai-refine --ai-provider anthropic + +# OpenAI (GPT) +export OPENAI_API_KEY=sk-... +flet-pkg create -f shimmer --ai-refine --ai-provider openai + +# Google (Gemini) +export GOOGLE_API_KEY=... +flet-pkg create -f shimmer --ai-refine --ai-provider google +``` + +### How AI Refinement Works + +The pipeline runs in 4 steps: + +``` +Step 1: Gap Report (deterministic, no LLM) +├── Compares the Dart API against the generated code +├── Identifies: missing methods, properties, enums, events, type mismatches +└── Produces a structured GapReport + +Step 2: Architect (LLM) +├── Receives the GapReport + Dart source snippets + generated file summaries +├── Reasons about HOW to fix each feasible gap +└── Produces an ArchitectPlan (text descriptions of improvements) + +Step 3: Editor (LLM) +├── Receives the ArchitectPlan + actual file contents +├── Produces precise search/replace code edits +└── Returns structured FileEdit objects + +Step 4: Validator (deterministic, no LLM) +├── Applies the edits to the generated files +├── Validates Python syntax +├── If errors: feeds back to Editor (max 2 retries) +└── Writes the refined files +``` + +--- + +## MCP Server + +flet-pkg includes an **MCP (Model Context Protocol) server** that exposes its scaffolding and code generation capabilities to AI agents like Claude Desktop, Cursor, and VS Code Copilot. This complements the CLI by allowing AI assistants to create Flet extensions programmatically. + +### MCP Installation + +```bash +# Install with MCP support +uv add flet-pkg[mcp] # or: pip install flet-pkg[mcp] + +# Verify it works +flet-pkg-mcp +``` + +### Claude Desktop Configuration + +Add to your Claude Desktop config (`~/.config/claude/claude_desktop_config.json`): + +```json +{ + "mcpServers": { + "flet-pkg": { + "command": "flet-pkg-mcp" + } + } +} +``` + +Or use `uvx` (no install needed): + +```json +{ + "mcpServers": { + "flet-pkg": { + "command": "uvx", + "args": ["--from", "flet-pkg[mcp]", "flet-pkg-mcp"] + } + } +} +``` + +### Available Tools + +| Tool | Description | Network | +|------|-------------|---------| +| `tool_derive_names` | Derive project/package/control names from a Flutter package name | No | +| `tool_map_dart_type` | Convert a Dart type to its Python/Flet equivalent | No | +| `tool_fetch_metadata` | Get pub.dev package metadata (version, description, homepage) | Yes (cached) | +| `tool_detect_extension_type` | Auto-detect whether a package is a service or ui_control | Yes (cached) | +| `tool_scaffold` | Create a project skeleton from a template | No | +| `tool_run_pipeline` | Full pipeline: download, parse, analyze, generate, write | Yes (cached) | +| `tool_analyze_gaps` | Deterministic coverage gap analysis (no LLM) | Yes (cached) | + +### Resources & Prompts + +**Resources:** + +| URI | Description | +|-----|-------------| +| `flet-pkg://type-map` | Full Dart-to-Python type mapping table | +| `flet-pkg://templates` | Available template names (service, ui_control) | + +**Prompts:** + +| Prompt | Description | +|--------|-------------| +| `scaffold_service` | Step-by-step guide to scaffold a service extension | +| `scaffold_ui_control` | Step-by-step guide to scaffold a UI control extension | +| `analyze_package` | Guide to analyze coverage without scaffolding | + +### Example Prompts + +With the MCP server configured, use natural language in Claude Code or Claude Desktop: + +``` +> Use the flet-pkg tool to analyze the Flutter package "onesignal_flutter" and tell me + the extension type and coverage. +``` + +``` +> Use the flet-pkg tool to analyze the Flutter package "rive" and tell me the extension + type and coverage. +``` + +--- + +## Generated Project Structure + +After running `flet-pkg create`, you get a complete, ready-to-develop project: + +``` +flet-onesignal/ # Project root +├── pyproject.toml # Hatchling build + Flet metadata +├── README.md # Extension documentation +├── CHANGELOG.md +├── LICENSE +├── mkdocs.yml # MkDocs Material config +├── docs/ # Documentation source +│ ├── index.md +│ ├── getting-started.md +│ ├── api-reference.md +│ └── examples.md +├── tests/ +│ └── test_flet_onesignal.py # Pytest test file +├── examples/ +│ └── flet_onesignal_example/ # Working example app +│ ├── pyproject.toml +│ └── src/ +│ └── main.py # ft.run(main) example +└── src/ + ├── flet_onesignal/ # Python package + │ ├── __init__.py # Public exports + │ ├── onesignal.py # Main control (@ft.control + ft.Service) + │ ├── user.py # Sub-module (auto-detected from Dart API) + │ ├── types.py # Enums, dataclasses, type definitions + │ └── console.py # Debug console (optional) + └── flutter/ + └── flet_onesignal/ # Flutter package + ├── pubspec.yaml # Flutter dependencies + ├── analysis_options.yaml + └── lib/ + ├── flet_onesignal.dart + └── src/ + ├── extension.dart # Flet extension entry point + └── onesignal_service.dart # Dart bridge code +``` + +The files under `src/flet_onesignal/` and `src/flutter/.../lib/src/` are **auto-generated** from the Flutter package analysis. They contain real method mappings, type conversions, and event handlers — not just stubs. + +--- + +## Name Conflict Detection + +When you choose an extension name, flet-pkg automatically checks for existing packages across: + +- **PyPI** — Python Package Index +- **GitHub** — Public repositories +- **Flet SDK** — Official Flet monorepo packages + +If matches are found, you'll see a warning with links and a confirmation prompt before continuing. This helps avoid naming conflicts with existing packages. + +--- + +## Generation Pipeline + +The deterministic pipeline (no AI needed) follows these steps: + +``` +download → parse → analyze → generate → [AI refine] → write +``` + +| Step | Module | Description | +|------|--------|-------------| +| **Download** | `core/downloader.py` | Fetches the Flutter package from pub.dev. Caches locally at `~/.cache/flet-pkg/`. | +| **Parse** | `core/parser.py` | Parses Dart source files into a structured `DartPackageAPI` (classes, methods, constructors, enums, streams, getters). | +| **Analyze** | `core/analyzer.py` | Produces a `GenerationPlan` with method mappings, type conversions, event detection, sub-module grouping, compound widget detection, and sibling widget detection. | +| **Generate** | `core/generators/` | Five generators produce Python and Dart code: control, sub-modules, types, `__init__.py`, and Dart service/control. | +| **AI Refine** | `core/ai/` | _(Optional)_ Gap analysis + LLM-powered code improvement. | +| **Write** | `core/pipeline.py` | Writes files into the project directory, cleans up template stubs that were replaced by generated files. | + +Key analysis features: + +- **Stream event detection** — `Stream get onXxx` patterns become Python event handlers +- **Sub-module grouping** — Related methods are grouped into separate Python files (e.g. `user.py`, `notifications.py`) +- **Compound widget detection** — Typed sub-widget constructor params become `ft.Control` sub-classes +- **Platform interface resolution** — Downloads `*_platform_interface` packages to fill stub enum values +- **Type mapping** — Dart types are mapped to Python/Flet equivalents via `core/type_map.py` +- **Internal method filtering** — `toString`, `hashCode`, `setMock*` and other framework methods are excluded + +--- + +## Coverage + +The deterministic pipeline (without AI) achieves the following coverage on tested packages: + +| Package | Type | Coverage | +|---------|------|----------| +| `shared_preferences` | Service | 100.0% | +| `url_launcher` | UI Control | 100.0% | +| `local_auth` | Service | 100.0% | +| `geolocator` | Service | 100.0% | +| `share_plus` | Service | 100.0% | +| `image_picker` | Service | 100.0% | +| `onesignal_flutter` | Service | 97.6% | + +Coverage is measured as the percentage of public Dart API surface (methods, properties, enums, events) that is correctly mapped to Python code. + +--- + +## Examples + +### Create a service extension for shared_preferences + +```bash +flet-pkg create -t service -f shared_preferences +cd flet-shared-preferences +uv sync +``` + +### Create a UI control for shimmer with AI refinement + +```bash +uv add flet-pkg[ai] # or: pip install flet-pkg[ai] +ollama pull qwen2.5-coder +flet-pkg create -t ui_control -f shimmer --ai-refine +``` + +### Create from a local Flutter package + +```bash +flet-pkg create -t service -f my_package -l ./my_flutter_package +``` + +### Verbose output with coverage breakdown + +```bash +flet-pkg create -t service -f battery_plus -v +``` + +### Non-interactive mode for CI + +```bash +flet-pkg create \ + -t auto \ + -f onesignal_flutter \ + -o ./output \ + --no-console \ + --no-ai-refine +``` + +--- + +## Development + +```bash +# Clone and install +git clone https://github.com/brunobrown/flet-pkg.git +cd flet-pkg +uv sync + +# Run tests +uv run pytest tests/ -v + +# Lint and format +uv tool run ruff format +uv tool run ruff check +uv tool run ty check + +# Run the CLI locally +uv run flet-pkg create +``` + +--- + +## Learn more +* [Documentation](https://brunobrown.github.io/flet-pkg) + +## Flet Community + +Join the community to contribute or get help: + +* [Discussions](https://github.com/flet-dev/flet/discussions) +* [Discord](https://discord.gg/dzWXP8SHG8) +* [X (Twitter)](https://twitter.com/fletdev) +* [Bluesky](https://bsky.app/profile/fletdev.bsky.social) +* [Email us](mailto:hello@flet.dev) + +## Support + +If you like this project, please give it a [GitHub star](https://github.com/brunobrown/flet-pkg) ⭐ + +--- + +## Contributing + +Contributions and feedback are welcome! + +1. Fork the repository +2. Create a feature branch +3. Submit a pull request with detailed explanation + +For feedback, [open an issue](https://github.com/brunobrown/flet-pkg/issues) with your suggestions. + +--- +## Try flet-pkg today and turn any Flutter package into a Flet extension in seconds! + +

+

Commit your work to the LORD, and your plans will succeed. Proverbs 16:3

diff --git a/docs/api/ai.md b/docs/api/ai.md new file mode 100644 index 0000000..97eea91 --- /dev/null +++ b/docs/api/ai.md @@ -0,0 +1,17 @@ +# AI Refinement + +## Configuration + +::: flet_pkg.core.ai.config + +## Gap Analyzer + +::: flet_pkg.core.ai.gap_analyzer + +## Models + +::: flet_pkg.core.ai.models + +## Refiner + +::: flet_pkg.core.ai.refiner diff --git a/docs/api/analyzer.md b/docs/api/analyzer.md new file mode 100644 index 0000000..124a445 --- /dev/null +++ b/docs/api/analyzer.md @@ -0,0 +1,3 @@ +# Analyzer + +::: flet_pkg.core.analyzer diff --git a/docs/api/downloader.md b/docs/api/downloader.md new file mode 100644 index 0000000..82f7523 --- /dev/null +++ b/docs/api/downloader.md @@ -0,0 +1,3 @@ +# Downloader + +::: flet_pkg.core.downloader diff --git a/docs/api/generators.md b/docs/api/generators.md new file mode 100644 index 0000000..094984f --- /dev/null +++ b/docs/api/generators.md @@ -0,0 +1,21 @@ +# Generators + +## Python Control + +::: flet_pkg.core.generators.python_control + +## Python Sub-Modules + +::: flet_pkg.core.generators.python_submodule + +## Python Types + +::: flet_pkg.core.generators.python_types + +## Python Init + +::: flet_pkg.core.generators.python_init + +## Dart Service + +::: flet_pkg.core.generators.dart_service diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..085b4fc --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,29 @@ +# API Reference + +Auto-generated documentation from source code docstrings. + +## Core Modules + +| Module | Description | +|--------|-------------| +| [Pipeline](pipeline.md) | Orchestrates download → parse → analyze → generate → write | +| [Scaffolder](scaffolder.md) | Jinja2 template engine for project skeleton | +| [Validators](validators.md) | Name validation and derivation | +| [Models](models.md) | Data classes for Dart API and generation plan | +| [Type Map](type-map.md) | Dart → Python type mapping | +| [Downloader](downloader.md) | pub.dev package downloader with cache | +| [Parser](parser.md) | Dart API parser and extension type detection | +| [Analyzer](analyzer.md) | Dart-to-Python generation plan analyzer | +| [Generators](generators.md) | Python + Dart code generators | + +## AI Modules + +| Module | Description | +|--------|-------------| +| [AI Refinement](ai.md) | LLM-powered gap analysis and code improvement | + +## MCP + +| Module | Description | +|--------|-------------| +| [MCP Server](mcp.md) | Model Context Protocol server for AI agents | diff --git a/docs/api/mcp.md b/docs/api/mcp.md new file mode 100644 index 0000000..386a4d0 --- /dev/null +++ b/docs/api/mcp.md @@ -0,0 +1,3 @@ +# MCP Server + +::: flet_pkg.mcp.server diff --git a/docs/api/models.md b/docs/api/models.md new file mode 100644 index 0000000..1628dbe --- /dev/null +++ b/docs/api/models.md @@ -0,0 +1,3 @@ +# Models + +::: flet_pkg.core.models diff --git a/docs/api/parser.md b/docs/api/parser.md new file mode 100644 index 0000000..3680f6d --- /dev/null +++ b/docs/api/parser.md @@ -0,0 +1,3 @@ +# Parser + +::: flet_pkg.core.parser diff --git a/docs/api/pipeline.md b/docs/api/pipeline.md new file mode 100644 index 0000000..af883a4 --- /dev/null +++ b/docs/api/pipeline.md @@ -0,0 +1,3 @@ +# Pipeline + +::: flet_pkg.core.pipeline diff --git a/docs/api/scaffolder.md b/docs/api/scaffolder.md new file mode 100644 index 0000000..09936e6 --- /dev/null +++ b/docs/api/scaffolder.md @@ -0,0 +1,3 @@ +# Scaffolder + +::: flet_pkg.core.scaffolder diff --git a/docs/api/type-map.md b/docs/api/type-map.md new file mode 100644 index 0000000..78f3445 --- /dev/null +++ b/docs/api/type-map.md @@ -0,0 +1,3 @@ +# Type Map + +::: flet_pkg.core.type_map diff --git a/docs/api/validators.md b/docs/api/validators.md new file mode 100644 index 0000000..900873c --- /dev/null +++ b/docs/api/validators.md @@ -0,0 +1,3 @@ +# Validators + +::: flet_pkg.core.validators diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..5da79e1 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,167 @@ +# Architecture + +How `flet-pkg` works internally. + +## Overview + +``` +CLI (Typer) → Prompts (Rich) → Validators → Registry Check → Scaffolder (Jinja2) → Pipeline → AI Refine → Output +``` + +## Module structure + +``` +src/flet_pkg/ +├── __init__.py # Version and app name +├── __main__.py # python -m flet_pkg support +├── main.py # Typer app, version callback +├── commands/ +│ └── create.py # Create command logic +├── core/ +│ ├── analyzer.py # PackageAnalyzer → GenerationPlan +│ ├── downloader.py # PubDevDownloader (pub.dev cache) +│ ├── generators/ # Code generators (Python + Dart) +│ │ ├── base.py # CodeGenerator abstract base +│ │ ├── python_control.py # Main control file +│ │ ├── python_submodule.py # Sub-module files +│ │ ├── python_types.py # types.py (enums, events) +│ │ ├── python_init.py # __init__.py exports +│ │ └── dart_service.py # Dart FletService/FletWidget +│ ├── models.py # DartPackageAPI, GenerationPlan +│ ├── parser.py # Dart API parser + detect_extension_type() +│ ├── pipeline.py # GenerationPipeline orchestrator +│ ├── prompts.py # Rich interactive prompts +│ ├── registry_checker.py # PyPI, GitHub, Flet SDK name conflict check +│ ├── scaffolder.py # Jinja2 template engine +│ ├── type_map.py # Dart → Python type mapping +│ └── validators.py # Name validation + derivation +├── core/ai/ +│ ├── agent.py # pydantic-ai Architect/Editor agents +│ ├── config.py # AIConfig + provider detection +│ ├── gap_analyzer.py # Deterministic coverage gap analyzer +│ ├── models.py # GapReport, RefinementResult, etc. +│ ├── provider.py # Model factory for pydantic-ai +│ └── refiner.py # AIRefiner orchestrator +├── mcp/ +│ ├── server.py # FastMCP server (tools, resources, prompts) +│ └── _serializers.py # Dataclass → dict helpers +├── ui/ +│ ├── console.py # Rich console instance +│ ├── coverage.py # Coverage score + breakdown table +│ ├── panels.py # Header, info, error panels +│ └── tree.py # Project tree display +└── templates/ + ├── service/ # Service extension template + └── ui_control/ # UI Control extension template +``` + +## Key components + +### Typer CLI (`main.py`) + +The entry point registers the `create` command and a `--version` callback. Uses `rich_markup_mode="rich"` for styled help text. + +### Create command (`commands/create.py`) + +Orchestrates the creation flow: + +1. Determines extension type (from flag or interactive prompt). If `auto`, downloads the Flutter package and calls `detect_extension_type()` to resolve it. +2. Collects Flutter package name, project name, package name, control class name +3. Calls `derive_names()` to auto-suggest names +4. Checks PyPI, GitHub, and Flet SDK monorepo for name conflicts via `registry_checker` +5. Builds a context dict and passes it to `Scaffolder` +6. Runs the analysis pipeline (download → parse → analyze → generate) +7. Optionally runs AI refinement if `--ai-refine` is set +8. Displays coverage score and the generated project tree + +### Registry checker (`core/registry_checker.py`) + +Checks for existing packages with the same name before scaffolding: + +- **PyPI** — `GET https://pypi.org/pypi/{name}/json` (200 = exists) +- **Flet SDK** — checks `flet-dev/flet` monorepo at `sdk/python/packages/{name}` via GitHub Contents API +- **GitHub** — searches repositories matching the name (top 3 results) + +All checks fail silently on network errors to avoid blocking the flow. + +### Validators (`core/validators.py`) + +- `validate_flutter_package()` — valid pub.dev name +- `validate_project_name()` — lowercase + hyphens +- `validate_package_name()` — valid Python identifier +- `validate_control_name()` — PascalCase +- `derive_names()` — strips Flutter affixes and derives all names from the Flutter package name + +### Scaffolder (`core/scaffolder.py`) + +Uses Jinja2 to render templates: + +1. Walks the template directory tree +2. Resolves `{{variable}}` placeholders in directory and file names +3. Renders `.jinja` files through Jinja2 with the context dict +4. Copies non-Jinja files as-is + +### Generation Pipeline (`core/pipeline.py`) + +Orchestrates the full code generation flow: + +1. **Download** — fetches the Flutter package from pub.dev (or uses a local path) +2. **Parse** — extracts `DartPackageAPI` from Dart source files +3. **Analyze** — produces a `GenerationPlan` (methods, events, enums, properties, sub-modules) +4. **Generate** — runs 5 generators to produce Python + Dart files +5. **Write** — writes generated files to the project directory, overwriting template stubs +6. **Gap analysis** — computes coverage percentage +7. **AI refine** (optional) — runs the Architect/Editor pattern for improvements + +### Generators (`core/generators/`) + +Five specialized generators produce different parts of the output: + +| Generator | Output | +|-----------|--------| +| `PythonControlGenerator` | Main control file (e.g. `onesignal.py`) | +| `PythonSubModuleGenerator` | Sub-module files (e.g. `user.py`, `notifications.py`) | +| `PythonTypesGenerator` | `types.py` with enums and event dataclasses | +| `PythonInitGenerator` | `__init__.py` with exports | +| `DartServiceGenerator` | Dart service/widget files + `extension.dart` | + +### AI Refinement (`core/ai/`) + +Optional LLM-powered code improvement using the **Architect/Editor** pattern: + +1. **Gap Analyzer** (`gap_analyzer.py`) — deterministic comparison of `DartPackageAPI` vs `GenerationPlan` to find coverage gaps +2. **Architect** agent — LLM reasons about WHAT to improve based on the gap report +3. **Editor** agent — LLM produces search/replace edits (HOW to fix) +4. **Validator** — checks syntax of edited files and retries on failure + +Supports multiple providers: Ollama (local, free), Anthropic, OpenAI, Google. + +### Coverage + +The gap analyzer computes a coverage score: + +``` +coverage = generated_items / total_dart_api_items × 100 +``` + +Categories tracked: Methods, Events, Enums, Properties. + +### MCP Server (`mcp/server.py`) + +Exposes flet-pkg capabilities to AI agents via the [Model Context Protocol](https://modelcontextprotocol.io/): + +- **7 tools**: derive_names, map_dart_type, fetch_metadata, detect_extension_type, scaffold, run_pipeline, analyze_gaps +- **2 resources**: type-map, templates +- **3 prompts**: scaffold_service, scaffold_ui_control, analyze_package + +See the [MCP Server documentation](mcp-server.md) for configuration details. + +### Templates + +Templates live under `src/flet_pkg/templates/`. Each template type has: + +- `template.yaml` — template metadata +- A directory tree with `{{variable}}` placeholders in names +- `.jinja` suffix on files that need rendering + +Variable substitution happens both in **file/directory names** and in **file contents** (for `.jinja` files). diff --git a/docs/assets/javascripts/external-links.js b/docs/assets/javascripts/external-links.js new file mode 100644 index 0000000..19662e3 --- /dev/null +++ b/docs/assets/javascripts/external-links.js @@ -0,0 +1,22 @@ +// Open all external links in a new tab. +// Uses MutationObserver to handle MkDocs Material instant navigation, +// which replaces DOM content without full page reloads. +(function () { + function processLinks() { + document.querySelectorAll("a[href^='http']").forEach(function (link) { + if (!link.hostname || link.hostname === location.hostname) return; + link.setAttribute("target", "_blank"); + link.setAttribute("rel", "noopener noreferrer"); + }); + } + + // Run on initial load + processLinks(); + + // Re-run whenever MkDocs Material swaps page content + var observer = new MutationObserver(processLinks); + var content = document.querySelector("[data-md-component=content]"); + if (content) { + observer.observe(content, { childList: true, subtree: true }); + } +})(); diff --git a/docs/assets/logo.png b/docs/assets/logo.png new file mode 100644 index 0000000..730533d Binary files /dev/null and b/docs/assets/logo.png differ diff --git a/docs/assets/proverbs.png b/docs/assets/proverbs.png new file mode 100644 index 0000000..78f2e96 Binary files /dev/null and b/docs/assets/proverbs.png differ diff --git a/docs/assets/stylesheets/extra.css b/docs/assets/stylesheets/extra.css new file mode 100644 index 0000000..37cd07c --- /dev/null +++ b/docs/assets/stylesheets/extra.css @@ -0,0 +1,23 @@ +/* flet-pkg brand: #DA8A17 */ + +[data-md-color-scheme="default"] { + --md-primary-fg-color: #DA8A17; + --md-primary-fg-color--light: #E5A43E; + --md-primary-fg-color--dark: #B87212; + --md-accent-fg-color: #DA8A17; + --md-accent-fg-color--transparent: #DA8A171a; +} + +[data-md-color-scheme="slate"] { + --md-primary-fg-color: #DA8A17; + --md-primary-fg-color--light: #E5A43E; + --md-primary-fg-color--dark: #B87212; + --md-accent-fg-color: #E5A43E; + --md-accent-fg-color--transparent: #E5A43E1a; +} + +/* White logo in the header and mobile drawer to match the site name text */ +.md-header .md-header__button.md-logo img, +.md-nav__title .md-nav__button.md-logo img { + filter: brightness(0) invert(1); +} diff --git a/docs/cli-reference.md b/docs/cli-reference.md new file mode 100644 index 0000000..1278d99 --- /dev/null +++ b/docs/cli-reference.md @@ -0,0 +1,147 @@ +# CLI Reference + +## Global options + +```bash +flet-pkg --version # Show version and exit +flet-pkg --help # Show help +flet-pkg create --help # Show help +``` + +## `flet-pkg create` + +Create a new Flet extension package. + +```bash +flet-pkg create [OPTIONS] +``` + +### Package Options + +| Option | Short | Type | Default | Description | +|--------|-------|------|---------|-------------| +| `--type` | `-t` | `TEXT` | *(interactive)* | Extension type: `auto`, `service` or `ui_control` | +| `--flutter-package` | `-f` | `TEXT` | *(interactive)* | Flutter package name from pub.dev | +| `--output` | `-o` | `PATH` | Current dir | Output directory | +| `--local-package` | `-l` | `PATH` | | Path to a local Flutter package (skip download) | + +### Code Generation Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `--analyze/--no-analyze` | `BOOL` | `True` | Analyze Flutter package and generate rich code | +| `--console/--no-console` | `BOOL` | *(interactive)* | Include a debug console module for development logging | + +### AI Refinement Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `--ai-refine/--no-ai-refine` | `BOOL` | *(interactive)* | Run AI-powered refinement on generated code | +| `--ai-provider` | `TEXT` | `ollama` | AI provider: `ollama`, `anthropic`, `openai`, or `google` | +| `--ai-model` | `TEXT` | *(auto)* | Override the default model for the selected provider | + +Default models per provider: + +| Provider | Default Model | +|----------|--------------| +| `ollama` | `qwen2.5-coder:14b` | +| `anthropic` | `claude-sonnet-4-6` | +| `openai` | `gpt-4.1-mini` | +| `google` | `gemini-2.5-flash` | + +### Output Options + +| Option | Short | Type | Default | Description | +|--------|-------|------|---------|-------------| +| `--verbose` | `-v` | `BOOL` | `False` | Show detailed analysis output and coverage breakdown table | + +#### `--type` values + +| Value | Description | +|-------|-------------| +| `auto` | Downloads the Flutter package and auto-detects the type (recommended) | +| `service` | Non-visual extension (`ft.Service`) | +| `ui_control` | Visual widget (`ft.LayoutControl`) | + +### Interactive mode + +When options are omitted, `flet-pkg` prompts you interactively using Rich: + +```bash +flet-pkg create +``` + +The interactive prompt presents three choices for extension type: + +``` +? Extension type: + 1 - Auto-detect (recommended) + 2 - Service (no visual interface) + 3 - UI Control (visual widget) +``` + +After you enter the extension name, `flet-pkg` checks **PyPI**, **GitHub**, and the **Flet SDK monorepo** for existing packages. If matches are found, you'll see a warning with links before a confirmation prompt. + +If `pydantic-ai` is installed (via `flet-pkg[ai]`), you'll also be prompted for AI refinement options. + +### Non-interactive mode + +Pass all required options via flags: + +```bash +flet-pkg create --type auto --flutter-package onesignal_flutter --output . +``` + +### Examples + +Auto-detect extension type: + +```bash +flet-pkg create -t auto -f onesignal_flutter +``` + +Create a service extension: + +```bash +flet-pkg create -t service -f onesignal_flutter +``` + +Create a UI control extension in a specific directory: + +```bash +flet-pkg create -t ui_control -f flutter_map -o ~/projects +``` + +Use a local Flutter package (skip download): + +```bash +flet-pkg create -t auto -f my_package -l ./path/to/my_package +``` + +With AI refinement (Ollama, local): + +```bash +flet-pkg create -f shimmer --ai-refine +``` + +With AI refinement (Anthropic): + +```bash +export ANTHROPIC_API_KEY=sk-... +flet-pkg create -f shimmer --ai-refine --ai-provider anthropic +``` + +Verbose output with coverage breakdown: + +```bash +flet-pkg create -f onesignal_flutter -v +``` + +### Validation rules + +| Field | Rule | +|-------|------| +| Flutter package | Lowercase, underscores, starts with letter (`[a-z][a-z0-9_]*`) | +| Project name | Lowercase, hyphens, starts with letter (`[a-z][a-z0-9-]*`) | +| Package name | Valid Python identifier (`[a-z][a-z0-9_]*`) | +| Control class name | PascalCase (`[A-Z][a-zA-Z0-9]*`) | diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..dbad752 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,80 @@ +# Contributing + +## Development setup + +1. Clone the repository: + + ```bash + git clone https://github.com/brunobrown/flet-pkg.git + cd flet-pkg + ``` + +2. Install dependencies with [uv](https://docs.astral.sh/uv/): + + ```bash + uv sync --dev + ``` + +3. Install docs dependencies (optional): + + ```bash + uv sync --group docs + ``` + +## Running tests + +```bash +uv run pytest -v +``` + +With coverage: + +```bash +uv run pytest -v --cov=flet_pkg --cov-report=term-missing +``` + +## Code quality + +Format code: + +```bash +uv run ruff format +``` + +Check linting: + +```bash +uv run ruff check +``` + +Type checking: + +```bash +uv run ty check +``` + +## Documentation + +Serve docs locally: + +```bash +uv run --group docs mkdocs serve +``` + +Build docs: + +```bash +uv run --group docs mkdocs build --strict +``` + +## Project structure + +See the [Architecture](architecture.md) page for details on how the codebase is organized. + +## Pull requests + +1. Create a feature branch from `main` +2. Make your changes +3. Ensure all checks pass (ruff format, ruff check, pytest) +4. Bump the version in `pyproject.toml` and `src/flet_pkg/__init__.py` +5. Open a PR against `main` diff --git a/docs/extension-templates/service.md b/docs/extension-templates/service.md new file mode 100644 index 0000000..6ca91d2 --- /dev/null +++ b/docs/extension-templates/service.md @@ -0,0 +1,64 @@ +# Service Template + +The **Service** template generates a non-visual Flet extension based on `ft.Service`. Use this for wrapping Flutter packages that don't have a UI component — e.g. push notifications, analytics, authentication. + +## Generated structure + +``` +flet-/ +├── pyproject.toml +├── README.md +├── CHANGELOG.md +├── LICENSE +├── mkdocs.yml +├── docs/ +│ ├── index.md +│ ├── getting-started.md +│ ├── api-reference.md +│ └── examples.md +├── tests/ +│ └── test_.py +├── examples/ +│ └── _example/ +│ ├── pyproject.toml +│ └── src/main.py +└── src/ + ├── / + │ ├── __init__.py + │ ├── .py # @ft.control + ft.Service + │ └── types.py + └── flutter/ + └── / + ├── pubspec.yaml + ├── analysis_options.yaml + ├── __init__.py + └── lib/ + ├── .dart + └── src/ + ├── extension.dart + └── _service.dart +``` + +## Python side + +The generated Python control uses: + +- `@ft.control("ControlName")` decorator to register the control +- `ft.Service` as the base class (non-visual) +- `await self._invoke_method("method_name", {"arg": value})` for Python-to-Dart calls + +## Dart side + +The generated Flutter code uses: + +- `FletExtension.createService()` to register the service +- `control.addInvokeMethodListener()` to handle method calls from Python +- `control.triggerEvent()` to send events back to Python + +## Flet patterns + +The Service template follows Flet 0.80.x+ extension patterns: + +- **setuptools** build system (required for `package-data` Flutter bundling) +- Python and Dart code live side-by-side under `src/` +- The Flutter package is bundled automatically during `flet build` diff --git a/docs/extension-templates/ui-control.md b/docs/extension-templates/ui-control.md new file mode 100644 index 0000000..5f65f11 --- /dev/null +++ b/docs/extension-templates/ui-control.md @@ -0,0 +1,67 @@ +# UI Control Template + +The **UI Control** template generates a visual Flet extension based on `ft.LayoutControl`. Use this for wrapping Flutter widgets — e.g. maps, charts, video players, custom UI components. + +## Generated structure + +``` +flet-/ +├── pyproject.toml +├── README.md +├── CHANGELOG.md +├── LICENSE +├── mkdocs.yml +├── docs/ +│ ├── index.md +│ ├── getting-started.md +│ ├── api-reference.md +│ └── changelog.md +├── tests/ +│ └── test_.py +├── examples/ +│ └── _example/ +│ ├── pyproject.toml +│ └── src/main.py +└── src/ + ├── / + │ ├── __init__.py + │ ├── .py # @ft.control + ft.LayoutControl + │ └── types.py + └── flutter/ + └── / + ├── pubspec.yaml + ├── analysis_options.yaml + ├── __init__.py + └── lib/ + ├── .dart + └── src/ + ├── extension.dart + └── _control.dart +``` + +## Python side + +The generated Python control uses: + +- `@ft.control("ControlName")` decorator to register the control +- `ft.LayoutControl` as the base class (visual widget with layout) +- `await self._invoke_method("method_name", {"arg": value})` for Python-to-Dart calls +- `ft.EventHandler[CustomEvent]` for Dart-to-Python events + +## Dart side + +The generated Flutter code uses: + +- `FletExtension.createWidget()` to register the widget +- `control.addInvokeMethodListener()` to handle method calls from Python +- `control.triggerEvent()` to send events back to Python +- A Flutter `Widget` that renders the actual UI + +## Key difference from Service + +| | Service | UI Control | +|---|---------|-----------| +| Base class | `ft.Service` | `ft.LayoutControl` | +| Dart factory | `createService()` | `createWidget()` | +| Dart file suffix | `_service.dart` | `_control.dart` | +| Visual | No | Yes | diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..c92fa46 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,80 @@ +# Getting Started + +This guide walks you through creating your first Flet extension package. + +## Prerequisites + +- Python 3.11+ +- [uv](https://docs.astral.sh/uv/) (recommended) or pip + +## Installation + +```bash +uv tool install flet-pkg +``` + +Or with pip: + +```bash +pip install flet-pkg +``` + +## Creating your first extension + +### 1. Run the create command + +```bash +flet-pkg create +``` + +You'll be prompted for: + +1. **Extension type** — Auto-detect (recommended), Service (non-visual), or UI Control (visual widget) +2. **Flutter package name** — the pub.dev package you're wrapping (e.g. `onesignal_flutter`) +3. **Extension name** — auto-derived, e.g. `flet-onesignal` +4. **Python package name** — auto-derived, e.g. `flet_onesignal` +5. **Control class name** — auto-derived, e.g. `Onesignal` +6. **Description** and **Author** + +!!! tip "Auto-detect" + When you choose **Auto-detect**, `flet-pkg` downloads the Flutter package and inspects the Dart source code to determine whether it contains widgets (UI Control) or only services. This is the recommended option if you're unsure which type to pick. + +!!! note "Name conflict detection" + After you enter the extension name, `flet-pkg` checks **PyPI**, **GitHub**, and the official **Flet SDK monorepo** (`flet-dev/flet`) for existing packages with the same name. If matches are found, links are displayed and you can choose whether to continue. + +### 2. Navigate to your project + +```bash +cd flet-onesignal +``` + +### 3. Install dependencies + +```bash +uv sync +``` + +### 4. Run tests + +```bash +uv run pytest -v +``` + +## Name derivation + +`flet-pkg` automatically derives names from the Flutter package name: + +| Flutter package | Project name | Python package | Class name | +|----------------|-------------|----------------|------------| +| `onesignal_flutter` | `flet-onesignal` | `flet_onesignal` | `Onesignal` | +| `flutter_map` | `flet-map` | `flet_map` | `Map` | +| `google_maps_flutter` | `flet-google-maps` | `flet_google_maps` | `GoogleMaps` | + +Common Flutter affixes (`flutter_`, `_flutter`, `_plus`, `_pro`) are stripped automatically. + +## What's next + +- Edit the Python control file in `src//` to implement your Flet control +- Edit the Dart service/widget in `src/flutter//lib/src/` +- Add examples in `examples/` +- Run `flet-pkg --help` for all available options diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..6b15995 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,129 @@ +

flet-pkg

+ +

flet-pkg

+ +**CLI tool to scaffold Flet extension packages.** + +Generates complete, ready-to-develop Flet extensions with Python + Flutter/Dart code, following **Flet 0.80.x+** patterns. + +!!! warning "Important: this tool does not perform miracles. 🙏😂" + + Every Flutter package on [pub.dev](https://pub.dev) has its own API surface, configuration requirements, and platform-specific behaviors. Some packages expose simple method calls; others require complex initialization flows, native platform setup (Android manifests, iOS plists, Gradle/CocoaPods configuration), or callback-based architectures that don't map cleanly to Python. + + It would be unrealistic to write a code generation algorithm that generalizes perfectly for every possible Flutter package. **flet-pkg** aims to accelerate the process by delivering significantly more than a blank skeleton — it downloads the real Dart source, parses the public API, and auto-generates Python controls, type mappings, enum definitions, event handlers, and Dart bridge code. For service packages, this typically covers ~95% of the API surface. + + However, **the developer is still responsible for**: + + - Understanding how the original Flutter/Dart package works + - Reviewing the generated code for correctness + - Handling package-specific configurations (native platform setup, initialization flows, etc.) + - Adjusting or complementing the generated code for edge cases the pipeline could not cover + + Think of flet-pkg as a smart starting point, not a finished product. + +## Features + +- **Interactive & non-interactive modes** — guided prompts or full CLI flags +- **Auto-detect extension type** — downloads the Flutter package and detects whether it's a Service or UI Control automatically +- **Three extension types** — Auto-detect (recommended), Service (non-visual), and UI Control (visual widget) +- **Name conflict detection** — checks PyPI, GitHub, and the Flet SDK monorepo before creating, warns if the name already exists +- **Complete project scaffolding** — Python package, Flutter package, tests, docs, examples +- **Smart name derivation** — automatically derives project, package, and class names from the Flutter package name +- **Flet 0.80.x+ patterns** — uses `@ft.control`, `ft.Service`, `ft.LayoutControl`, `invoke_method`, and `EventHandler` +- **AI refinement** — optional LLM-powered code improvement using the Architect/Editor pattern +- **[MCP Server](mcp-server.md)** — expose scaffolding and analysis tools to AI agents via the Model Context Protocol + +## Quick install + +```bash +pip install flet-pkg +``` + +Or with [uv](https://docs.astral.sh/uv/): + +```bash +uv tool install flet-pkg +``` + +## Quick start + +```bash +flet-pkg create +``` + +This walks you through creating a new Flet extension with interactive prompts. + +For non-interactive usage (auto-detect type): + +```bash +flet-pkg create --type auto --flutter-package onesignal_flutter --output . +``` + +Or specify the type explicitly: + +```bash +flet-pkg create --type service --flutter-package onesignal_flutter --output . +``` + +### With AI refinement + +```bash +uv add flet-pkg[ai] +ollama pull qwen2.5-coder:14b +flet-pkg create -f shimmer --ai-refine +``` + +## Generated project structure + +``` +flet-onesignal/ +├── pyproject.toml +├── README.md +├── CHANGELOG.md +├── LICENSE +├── mkdocs.yml +├── docs/ +├── tests/ +├── examples/ +│ └── flet_onesignal_example/ +└── src/ + ├── flet_onesignal/ # Python package + │ ├── __init__.py + │ ├── onesignal.py # @ft.control + ft.Service + │ └── types.py + └── flutter/ + └── flet_onesignal/ # Flutter package + ├── pubspec.yaml + └── lib/ + └── src/ + ├── extension.dart + └── onesignal_service.dart +``` + +## Learn more + +- [CLI Reference](cli-reference.md) — all flags and options +- [MCP Server](mcp-server.md) — use flet-pkg from AI agents +- [Architecture](architecture.md) — how flet-pkg works internally +- [API Reference](api/index.md) — source code documentation + +--- + +## Support the Project + +If you find this project useful, consider giving it a [star on GitHub](https://github.com/brunobrown/flet-pkg) and supporting its development: + + +Buy Me a Coffee + + +[:fontawesome-brands-github: GitHub](https://github.com/brunobrown) · [:fontawesome-brands-x-twitter: X](https://x.com/BrunoBrown86) · [:fontawesome-brands-linkedin: LinkedIn](https://linkedin.com/in/bruno-brown-29418167/) + +--- + +## Try **flet-pkg** today and turn any Flutter package into a Flet extension in seconds! + +--- + +

Proverbs 16:3

+

Commit your work to the LORD, and your plans will succeed. Proverbs 16:3

diff --git a/docs/mcp-server.md b/docs/mcp-server.md new file mode 100644 index 0000000..38c3d4f --- /dev/null +++ b/docs/mcp-server.md @@ -0,0 +1,222 @@ +# MCP Server + +flet-pkg includes a [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that exposes scaffolding and code generation capabilities to AI agents like Claude. + +## Installation + +The MCP server is included with flet-pkg. You also need the `mcp` dependency: + +```bash +uv add flet-pkg[mcp] +``` + +## Running standalone + +```bash +flet-pkg-mcp +``` + +This starts the server using stdio transport (the standard for MCP). + +## Configuration + +### Claude Code + +Add to your project's `.mcp.json` or `~/.claude/settings.json`: + +```json +{ + "mcpServers": { + "flet-pkg": { + "command": "uv", + "args": ["run", "--from", "flet-pkg[mcp]", "flet-pkg-mcp"] + } + } +} +``` + +### Claude Desktop + +Add to your Claude Desktop configuration file: + +=== "macOS" + + `~/Library/Application Support/Claude/claude_desktop_config.json` + +=== "Windows" + + `%APPDATA%\Claude\claude_desktop_config.json` + +```json +{ + "mcpServers": { + "flet-pkg": { + "command": "uv", + "args": ["run", "--from", "flet-pkg[mcp]", "flet-pkg-mcp"] + } + } +} +``` + +## Tools + +The server exposes 7 tools: + +### `tool_derive_names` + +Derive project/package/control names from a Flutter package name. + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `flutter_package` | `str` | Flutter package name (e.g. `onesignal_flutter`) | + +**Returns:** `project_name`, `package_name`, `control_name`, `control_name_snake` + +### `tool_map_dart_type` + +Convert a Dart type string to its Python/Flet equivalent. + +**Parameters:** + +| Name | Type | Default | Description | +|------|------|---------|-------------| +| `dart_type` | `str` | | Dart type to convert | +| `flet_aware` | `bool` | `False` | Use native Flet types for UI controls | + +### `tool_fetch_metadata` + +Get pub.dev package metadata (version, description, homepage). + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `flutter_package` | `str` | Flutter package name | + +### `tool_detect_extension_type` + +Auto-detect whether a Flutter package should be a `service` or `ui_control` extension. + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `flutter_package` | `str` | Flutter package name | + +**Returns:** `extension_type` (`"service"` or `"ui_control"`) + +### `tool_scaffold` + +Create a project skeleton from a template. Does **not** generate code — use `tool_run_pipeline` after this. + +**Parameters:** + +| Name | Type | Default | Description | +|------|------|---------|-------------| +| `template_name` | `str` | | `service` or `ui_control` | +| `flutter_package` | `str` | | Flutter package name | +| `project_name` | `str` | | Project directory name | +| `package_name` | `str` | | Python package name | +| `control_name` | `str` | | PascalCase control class name | +| `description` | `str` | `""` | Package description | +| `author` | `str` | `""` | Author name | +| `output_dir` | `str` | `"."` | Output directory path | +| `include_console` | `bool` | `True` | Include debug console module | + +### `tool_run_pipeline` + +Run the full generation pipeline: download → parse → analyze → generate → write. + +**Parameters:** + +| Name | Type | Default | Description | +|------|------|---------|-------------| +| `flutter_package` | `str` | | Flutter package name | +| `control_name` | `str` | | PascalCase control class name | +| `extension_type` | `str` | | `service` or `ui_control` | +| `project_dir` | `str` | | Path to scaffolded project | +| `package_name` | `str` | | Python package name | +| `description` | `str` | `""` | Package description | +| `control_name_snake` | `str` | `""` | Snake-case control name | +| `include_console` | `bool` | `True` | Include debug console module | +| `local_package_path` | `str \| None` | `None` | Local Flutter package path | + +### `tool_analyze_gaps` + +Analyze coverage gaps between a Flutter package and its generated code. Purely deterministic — no LLM required. + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| `flutter_package` | `str` | Flutter package name | +| `extension_type` | `str` | `service` or `ui_control` | + +**Returns:** `coverage_pct`, `total_dart_api`, `total_generated`, `feasible_gaps`, and a list of `gaps`. + +## Resources + +### `flet-pkg://type-map` + +Full Dart-to-Python type mapping table, including standard mappings, Flet-aware mappings, and skipped types. + +### `flet-pkg://templates` + +List of available template names and their paths. + +## Prompts + +### `scaffold_service` + +Step-by-step guide to scaffold a Flet service extension. Takes `flutter_package` as input. + +### `scaffold_ui_control` + +Step-by-step guide to scaffold a Flet UI control extension. Takes `flutter_package` as input. + +### `analyze_package` + +Guide to analyze a Flutter package's coverage without scaffolding. Takes `flutter_package` and `extension_type`. + +## Example prompts + +With the MCP server configured, you can use natural language prompts in Claude Code or Claude Desktop: + +### Analyze a service package + +``` +> Use the flet-pkg tool to analyze the Flutter package "onesignal_flutter" and tell me + the extension type and coverage. +``` + +Claude will call: + +1. `tool_detect_extension_type("onesignal_flutter")` → `service` +2. `tool_analyze_gaps("onesignal_flutter", "service")` → Coverage 95.4% + +### Analyze a UI control package + +``` +> Use the flet-pkg tool to analyze the Flutter package "rive" and tell me the extension + type and coverage. +``` + +Claude will call: + +1. `tool_detect_extension_type("rive")` → `ui_control` +2. `tool_analyze_gaps("rive", "ui_control")` → Coverage report + +### Scaffold a full project + +``` +> Use flet-pkg to scaffold a service extension for shared_preferences. +``` + +Claude will call: + +1. `tool_derive_names("shared_preferences")` +2. `tool_fetch_metadata("shared_preferences")` +3. `tool_scaffold(template_name="service", ...)` +4. `tool_run_pipeline(extension_type="service", ...)` diff --git a/flet_pkg/downloader.py b/flet_pkg/downloader.py deleted file mode 100644 index b8ea144..0000000 --- a/flet_pkg/downloader.py +++ /dev/null @@ -1,88 +0,0 @@ -import os -import requests -import tarfile -import tempfile -import shutil -from pathlib import Path - - -def download_package_from_pubdev(package_name: str, output_dir: Path) -> Path: - """ - Downloads and extracts a Flutter package from pub.dev to the specified directory. - - Args: - package_name: Name of the package (e.g., "url_launcher") - output_dir: Directory where the package should be extracted - - Returns: - Path to the extracted package directory - """ - - # Create output directory if it doesn't exist - output_dir.mkdir(parents=True, exist_ok=True) - - # Get package metadata - api_url = f"https://pub.dev/api/packages/{package_name}" - response = requests.get(api_url) - response.raise_for_status() - - data = response.json() - latest_version = data["latest"]["version"] - tarball_url = f"https://pub.dev/packages/{package_name}/versions/{latest_version}.tar.gz" - - print(f"📦 Downloading {package_name} v{latest_version}...") - - with tempfile.TemporaryDirectory() as tmp_dir: - # Download the package - tarball_path = Path(tmp_dir) / f"{package_name}.tar.gz" - with requests.get(tarball_url, stream=True) as r: - r.raise_for_status() - with open(tarball_path, "wb") as f: - for chunk in r.iter_content(chunk_size=8192): - f.write(chunk) - - # Extract the package - with tarfile.open(tarball_path, "r:gz") as tar: - # Extract to temporary directory - temp_extract_dir = Path(tmp_dir) / "extracted" - tar.extractall(path=temp_extract_dir, filter='data') - - # Find the actual package content (handles different package structures) - package_content = None - possible_paths = [ - temp_extract_dir / f"{package_name}-{latest_version}", - temp_extract_dir / package_name, - temp_extract_dir - ] - - for path in possible_paths: - if path.exists(): - package_content = path - break - - if not package_content: - raise FileNotFoundError("Could not find package content in extracted files") - - # Move content to output directory - for item in package_content.glob("*"): - dest = output_dir / item.name - if dest.exists(): - if dest.is_dir(): - shutil.rmtree(dest) - else: - os.remove(dest) - - shutil.move(str(item), str(dest)) - - print(f"✅ Package successfully extracted to: {output_dir}") - print("📦 Package contents:") - for item in output_dir.glob("*"): - print(f" - {item.name}") - - return output_dir - - -if __name__ == "__main__": - package_name = "flame" - output_dir = Path("package/flutter") - download_package_from_pubdev(package_name, output_dir) diff --git a/flet_pkg/main.py b/flet_pkg/main.py deleted file mode 100644 index 6d5571f..0000000 --- a/flet_pkg/main.py +++ /dev/null @@ -1,147 +0,0 @@ -import re -from pathlib import Path -from pprint import pprint -from typing import Dict, Any -from flet_pkg.downloader import download_package_from_pubdev -from jinja2 import Environment, FileSystemLoader, TemplateNotFound - - -def generate_python_flet_wrapper(package_name: str, api_info: Dict[str, Any], output_dir: Path) -> None: - """ - Generates Python wrapper code from parsed Dart API info using Jinja2 templates. - Enhanced version with better error handling and file naming. - - Args: - package_name: Name of the package (used for naming and imports) - api_info: Dictionary with parsed class and method info from Dart source - output_dir: Path where the wrapper files will be saved - - Raises: - ValueError: If api_info has invalid structure - FileNotFoundError: If template directory is missing - TemplateNotFound: If template file is missing - """ - - # Validate api_info structure - if not isinstance(api_info, dict) or not all( - isinstance(cls_data, dict) and 'methods' in cls_data - for cls_data in api_info.values() - ): - raise ValueError("Invalid api_info structure") - - # Setup templates - template_dir = Path(__file__).parent / "templates" - if not template_dir.exists(): - raise FileNotFoundError(f"Template directory not found at {template_dir}") - - env = Environment( - loader=FileSystemLoader(str(template_dir)), - trim_blocks=True, - lstrip_blocks=True, - autoescape=False, - keep_trailing_newline=True, - extensions=["jinja2.ext.do"] - ) - - try: - template = env.get_template("wrapper_python_flet.py.j2") - except TemplateNotFound: - raise TemplateNotFound( - f"Wrapper template not found in {template_dir}. " - "Expected file: wrapper_python_flet.py.j2" - ) - - # Ensure output directory exists - output_dir.mkdir(parents=True, exist_ok=True) - - # Generate wrapper for each class - for class_name, class_data in api_info.items(): - # Sanitize filename - safe_class_name = re.sub(r'[^a-zA-Z0-9_]', '_', class_name) - filename = f"{package_name.lower()}_{safe_class_name.lower()}.py" - output_path = output_dir / filename - - # Add snake_case name to each method - for method in class_data.get("methods", []): - method["py_name"] = _camel_to_snake(method["name"]) - - try: - wrapper_code = template.render( - package_name=package_name, - class_name=class_name, - safe_class_name=safe_class_name, - class_doc=class_data.get("docstring", ""), - methods=class_data.get("methods", []), - source_file=class_data.get("source_file", "") - ) - - # Write to file with atomic write pattern - temp_path = output_path.with_suffix('.tmp') - with open(temp_path, 'w', encoding='utf-8') as f: - f.write(wrapper_code) - temp_path.replace(output_path) - - print(f"✅ Generated: {output_path}") - - except Exception as e: - print(f"❌ Failed to generate wrapper for {class_name}: {str(e)}") - continue - - -def generate_dart_flutter_wrapper(api_info: Dict[str, Any], package_name: str, widget_class_name: str, output_dir: Path) -> None: - template_dir = Path(__file__).parent / "templates" - if not template_dir.exists(): - raise FileNotFoundError(f"Template directory not found at {template_dir}") - - env = Environment( - loader=FileSystemLoader(str(template_dir)), - trim_blocks=True, - lstrip_blocks=True, - autoescape=False, - keep_trailing_newline=True - ) - - try: - template = env.get_template("wrapper_dart_flutter.dart.j2") - except TemplateNotFound: - raise TemplateNotFound("Template wrapper_dart_flutter.dart.j2 not found") - - all_methods = set() - for class_data in api_info.values(): - for method in class_data.get("methods", []): - all_methods.add(method["name"]) - - content = template.render(methods=sorted(all_methods), package_name=package_name, widget_class_name=widget_class_name) - - main_dart_path = output_dir / "lib" / f"flet_{package_name}.dart" - main_dart_path.parent.mkdir(parents=True, exist_ok=True) - with open(main_dart_path, "w", encoding="utf-8") as f: - f.write(content) - print(f"✅ Generated: {main_dart_path}") - - -def _camel_to_snake(name: str) -> str: - return re.sub(r'(? str: - return re.sub(r'(? str: - return name[0].lower() + name[1:] if name else name - -def should_skip_class(class_name: str, docstring: str = "") -> bool: - return ( - class_name.startswith('_') - or class_name.endswith(UI_CLASS_SUFFIXES) - or any(suffix in class_name for suffix in UI_CLASS_SUFFIXES) - or "nodoc" in docstring.lower() - or "internal" in docstring.lower() - ) - -def should_skip_method(method_name: str, docstring: str = "") -> bool: - return ( - method_name in UI_METHODS - or method_name in LIFECYCLE_METHODS - or method_name.startswith('_') - or method_name.startswith('on') - or "nodoc" in docstring.lower() - or not method_name.isidentifier() - or method_name[0].isupper() - ) - -def parse_dart_package(package_path: Path, strict: bool = False) -> Dict[str, Any]: - lib_dir = package_path / "lib" - if not lib_dir.exists(): - raise FileNotFoundError(f"Directory {lib_dir} does not exist.") - - api_info = {} - - for dart_file in lib_dir.rglob("*.dart"): - with open(dart_file, "r", encoding="utf-8") as file: - content = file.read() - - class_matches = re.finditer( - pattern=r'(?:@[^\n]*\n)*' - r'class\s+(\w+)' - r'(?:\s+extends\s+\w+)?' - r'(?:\s+with\s+[\w\s,]+)?' - r'(?:\s+implements\s+[\w\s,]+)?' - r'\s*{', - string=content - ) - - for match in class_matches: - class_name = match.group(1) - - doc_match = re.search(r'(///.*?$|/\*\*(.*?)\*/)', content[:match.start()], re.DOTALL | re.MULTILINE) - class_doc = clean_docstring(doc_match) if doc_match else "" - - if should_skip_class(class_name, class_doc): - continue - - class_block = extract_code_block(content[match.end()-1:]) - if not class_block: - continue - - methods = [] - seen_method_names = set() - - method_matches = re.finditer( - pattern=r'(?:@[^\n]*\n)*' - r'(?:static\s+)?' - r'(?:Future<.*?>|Future|void|String|bool|int|double|dynamic|Map<.*?>|List<.*?>|\w+)\s+' - r'(get\s+|set\s+|)?(\w+)' - r'\s*\(([^)]*)\)', - string=class_block - ) - - for m in method_matches: - is_getter = m.group(1) == 'get' - is_setter = m.group(1) == 'set' - method_name = m.group(2) - - method_doc_match = re.search(r'(///.*?$|/\*\*(.*?)\*/)', content[:m.start()], re.DOTALL | re.MULTILINE) - method_doc = clean_docstring(method_doc_match) if method_doc_match else "" - - if ( - method_name in seen_method_names - or should_skip_method(method_name, method_doc) - ): - continue - seen_method_names.add(method_name) - - if keyword.iskeyword(method_name): - method_name += "_" - - params = [] - if m.group(3): - for param in split_params(m.group(3)): - param = re.sub(r'\s*=\s*[^,]+', '', param).strip() - param = param.replace('??', '').replace('||', '').strip() - if param and param.split(' ')[-1].isidentifier(): - params.append(param) - - methods.append({ - "name": method_name, - "return_type": m.group(1) if not is_setter else "void", - "params": params, - "docstring": method_doc, - "is_getter": is_getter, - "is_setter": is_setter - }) - - filtered_methods = [ - m for m in methods if not should_skip_method(m['name'], m['docstring'] or "") - ] - - if strict and len(filtered_methods) < 2: - continue - - if filtered_methods: - api_info[class_name] = { - "methods": filtered_methods, - "docstring": class_doc, - "source_file": str(dart_file.relative_to(lib_dir)) - } - - return api_info - -def extract_code_block(content: str) -> Optional[str]: - brace_count = 1 - end_pos = 1 - for i, c in enumerate(content[1:], 1): - if c == '{': - brace_count += 1 - elif c == '}': - brace_count -= 1 - if brace_count == 0: - end_pos = i - break - return content[1:end_pos] if brace_count == 0 else None - -def split_params(param_str: str) -> List[str]: - params = [] - current = [] - paren_level = 0 - for char in param_str: - if char == ',' and paren_level == 0: - params.append(''.join(current).strip()) - current = [] - else: - if char == '(': - paren_level += 1 - elif char == ')': - paren_level -= 1 - current.append(char) - if current: - params.append(''.join(current).strip()) - return params - -def clean_docstring(match) -> Optional[str]: - if not match: - return None - doc = match.group(1) or match.group(2) or '' - doc = re.sub(r'///+', '', doc) - doc = re.sub(r'/\*\*|\*/', '', doc) - doc = doc.strip().replace('\n', ' ').replace(' ', ' ') - return doc - - -if __name__ == "__main__": - from pprint import pprint - api = parse_dart_package(Path("package/flutter")) - pprint(api) \ No newline at end of file diff --git a/flet_pkg/templates/wrapper_dart_flutter.dart.j2 b/flet_pkg/templates/wrapper_dart_flutter.dart.j2 deleted file mode 100644 index 3043f22..0000000 --- a/flet_pkg/templates/wrapper_dart_flutter.dart.j2 +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flet/flet.dart'; - - -class {{ widget_class_name }} extends StatefulWidget { - final Control control; - final Control? parent; - final FletControlBackend backend; - - const {{ widget_class_name }}({ - super.key, - required this.parent, - required this.control, - required this.backend, - }); - - @override - _{{ widget_class_name }}State createState() => _{{ widget_class_name }}State(); -} - -class _{{ widget_class_name }}State extends State<{{ widget_class_name }}> { - @override - Widget build(BuildContext context) { - print("{{ instance_name|capitalize }} build: \${widget.control.id} (\${widget.control.hashCode})"); - - () async { - widget.backend.subscribeMethods(widget.control.id, (methodName, args) async { - try { - return switch (methodName) { - {% for method in methods %} - "{{ method }}" => _{{ method }}(args), - {% endfor %} - _ => null, - }; - } catch (error, stackTrace) { - print("Error:\\nmethodName: \$methodName\\nerror: \$error\\nstackTrace: \$stackTrace"); - widget.backend.triggerControlEvent( - widget.control.id, - "error", - "methodName: \$methodName\\nerror: \$error\\nstackTrace: \$stackTrace", - ); - return error.toString(); - } - }); - }(); - - return const SizedBox.shrink(); - } - - // Auxiliares gerados dinamicamente - {% for method in methods %} - Future _{{ method }}(Map args) async { - print("Called {{ method }} with args: \$args"); - return "{{ method }} result"; - } - {% endfor %} -} \ No newline at end of file diff --git a/flet_pkg/templates/wrapper_python_flet.py.j2 b/flet_pkg/templates/wrapper_python_flet.py.j2 deleted file mode 100644 index 44af20e..0000000 --- a/flet_pkg/templates/wrapper_python_flet.py.j2 +++ /dev/null @@ -1,48 +0,0 @@ -from flet import Control -import json - - -class {{ class_name }}(Control): - """ - {{ class_doc | default("Wrapper class for " ~ class_name, true) }} - - Auto-generated from Dart source: {{ source_file }} - """ - - def _get_control_name(self): - return "{{ package_name }}" - - {% for method in methods %} - {% set param_names = [] %} - {% for param in method.params %} - {% set name = param.split(' ')[-1].replace('?', '') %} - {% if name.isidentifier() %} - {% do param_names.append(name) %} - {% endif %} - {% endfor %} - - def {{ method.py_name }}(self{% if param_names %}, {{ param_names | join(', ') }}{% endif %}, wait_timeout: float = 25): - """ - {{ method.docstring | default("No description.", true) }} - {% if method.is_getter %}Getter method.{% elif method.is_setter %}Setter method.{% endif %} - """ - args = json.dumps({ - {% for name in param_names %} - "{{ name }}": {{ name }}, - {% endfor %} - }) - return self.invoke_method("{{ method.name }}", {"data": args}, wait_for_result=True, wait_timeout=wait_timeout) - - async def {{ method.py_name }}_async(self{% if param_names %}, {{ param_names | join(', ') }}{% endif %}, wait_timeout: float = 25): - """ - {{ method.docstring | default("No description.", true) }} (async) - {% if method.is_getter %}Getter method.{% elif method.is_setter %}Setter method.{% endif %} - """ - args = json.dumps({ - {% for name in param_names %} - "{{ name }}": {{ name }}, - {% endfor %} - }) - return await self.invoke_method_async("{{ method.name }}", {"data": args}, wait_for_result=True, wait_timeout=wait_timeout) - -{% endfor %} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..a4b23b0 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,113 @@ +site_name: flet-pkg +site_description: CLI tool to scaffold Flet extension packages +site_url: https://brunobrown.github.io/flet-pkg +repo_url: https://github.com/brunobrown/flet-pkg +repo_name: brunobrown/flet-pkg + +theme: + name: material + logo: assets/logo.png + favicon: assets/logo.png + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: custom + accent: custom + toggle: + icon: material/brightness-7 + name: Dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: custom + accent: custom + toggle: + icon: material/brightness-4 + name: Light mode + features: + - navigation.tabs + - navigation.sections + - navigation.expand + - navigation.footer + - content.code.copy + - content.code.annotate + - search.highlight + - search.suggest + - toc.follow + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/brunobrown/flet-pkg + name: flet-pkg on GitHub + - icon: fontawesome/brands/discord + link: https://discord.gg/dzWXP8SHG8 + name: Community Discord + - icon: fontawesome/brands/x-twitter + link: https://x.com/fletdev + name: Flet on X + - icon: fontawesome/brands/bluesky + link: https://bsky.app/profile/fletdev.bsky.social + name: Flet on Bluesky + +extra_css: + - assets/stylesheets/extra.css + +extra_javascript: + - assets/javascripts/external-links.js + +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + docstring_style: google + show_source: true + show_root_heading: true + show_signature_annotations: true + merge_init_into_class: true + separate_signature: true + show_symbol_type_heading: true + show_symbol_type_toc: true + members_order: source + +markdown_extensions: + - attr_list + - def_list + - admonition + - pymdownx.details + - pymdownx.superfences + - pymdownx.snippets + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.tabbed: + alternate_style: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - toc: + permalink: "#" + +nav: + - Home: index.md + - Getting Started: getting-started.md + - CLI Reference: cli-reference.md + - MCP Server: mcp-server.md + - Templates: + - Service: extension-templates/service.md + - UI Control: extension-templates/ui-control.md + - Architecture: architecture.md + - API Reference: + - Overview: api/index.md + - Pipeline: api/pipeline.md + - Scaffolder: api/scaffolder.md + - Validators: api/validators.md + - Models: api/models.md + - Type Map: api/type-map.md + - Downloader: api/downloader.md + - Parser: api/parser.md + - Analyzer: api/analyzer.md + - Generators: api/generators.md + - AI Refinement: api/ai.md + - MCP Server: api/mcp.md + - Contributing: contributing.md diff --git a/pyproject.toml b/pyproject.toml index c846cb9..056685f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,102 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [project] name = "flet-pkg" -version = "0.0.0" -description = "Add your description here" -requires-python = ">=3.12" +version = "0.1.0" +description = "CLI tool to scaffold Flet extension packages" +requires-python = ">=3.10" +license = "MIT" +license-files = ["LICENSE"] +authors = [{ name = "Bruno Brown", email = "brunobrown.86@gmail.com" }] +keywords = ["flet", "flutter", "scaffold", "cli", "extension"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Software Development :: Code Generators", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed", +] dependencies = [ - "flet>=0.27.6", - "jinja2>=3.1.6", - "requests>=2.32.3", + "typer>=0.12.0", + "rich>=13.0.0", + "jinja2>=3.1.0", + "pyyaml>=6.0", + "httpx>=0.27.0", +] + +[project.optional-dependencies] +ai = ["pydantic-ai>=1.0.0"] +mcp = ["mcp[cli]>=1.0.0"] + +[project.scripts] +flet-pkg = "flet_pkg.main:app" +flet-pkg-mcp = "flet_pkg.mcp.server:main" + +[project.urls] +Homepage = "https://github.com/brunobrown/flet-pkg" +Documentation = "https://brunobrown.github.io/flet-pkg" +Repository = "https://github.com/brunobrown/flet-pkg" +Issues = "https://github.com/brunobrown/flet-pkg/issues" +Changelog = "https://github.com/brunobrown/flet-pkg/blob/main/CHANGELOG.md" + +[tool.hatch.build.targets.wheel] +packages = ["src/flet_pkg"] + +[dependency-groups] +dev = [ + "pytest>=8.0", + "pytest-cov>=7.0.0", + "pytest-asyncio>=0.24.0", + "mcp[cli]>=1.0.0", +] +docs = [ + "mkdocs>=1.6.0", + "mkdocs-material>=9.6.0", + "mkdocstrings[python]>=0.29.0", + "pymdown-extensions>=10.14.0", +] + +[tool.ruff] +line-length = 100 +target-version = "py311" +extend-exclude = ["flet-*"] + +[tool.ruff.format] +quote-style = "double" + +[tool.ruff.lint] +select = ["E", "W", "F", "I"] + +[tool.ruff.lint.isort] +known-first-party = ["flet_pkg"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = "-v --tb=short" +markers = [ + "network: tests that download packages from pub.dev (deselect with -m 'not network')", ] + +[tool.ty.rules] +possibly-unresolved-reference = "warn" +unresolved-reference = "error" +unresolved-import = "error" +invalid-type-form = "warn" + +[[tool.ty.overrides]] +# pydantic_ai is an optional dependency (flet-pkg[ai]) — not installed in CI. +include = ["src/flet_pkg/core/ai/**", "src/flet_pkg/commands/create.py"] +[tool.ty.overrides.rules] +unresolved-import = "ignore" diff --git a/src/flet_pkg/__init__.py b/src/flet_pkg/__init__.py new file mode 100644 index 0000000..508bd02 --- /dev/null +++ b/src/flet_pkg/__init__.py @@ -0,0 +1,2 @@ +__version__ = "0.1.0" +__app_name__ = "flet-pkg" diff --git a/src/flet_pkg/__main__.py b/src/flet_pkg/__main__.py new file mode 100644 index 0000000..2a9a636 --- /dev/null +++ b/src/flet_pkg/__main__.py @@ -0,0 +1,3 @@ +from flet_pkg.main import app + +app() diff --git a/flet_pkg/__init__.py b/src/flet_pkg/commands/__init__.py similarity index 100% rename from flet_pkg/__init__.py rename to src/flet_pkg/commands/__init__.py diff --git a/src/flet_pkg/commands/create.py b/src/flet_pkg/commands/create.py new file mode 100644 index 0000000..7fdb9ad --- /dev/null +++ b/src/flet_pkg/commands/create.py @@ -0,0 +1,500 @@ +"""Create command for flet-pkg. + +Orchestrates the full extension creation flow: type detection, name +derivation, registry checking, scaffolding, pipeline analysis, and +optional AI refinement. +""" + +from pathlib import Path +from typing import Optional + +import typer + +from flet_pkg.core.prompts import ask, ask_choice, ask_confirm +from flet_pkg.core.registry_checker import ( + RegistryMatch, + check_flet_packages, + check_github, + check_pypi, +) +from flet_pkg.core.scaffolder import Scaffolder +from flet_pkg.core.validators import ( + derive_names, + validate_control_name, + validate_flutter_package, + validate_package_name, + validate_project_name, +) +from flet_pkg.ui.console import console +from flet_pkg.ui.panels import error_panel, header_panel, info_panel +from flet_pkg.ui.tree import print_tree + +EXTENSION_TYPES = { + 1: ("auto", "Auto-detect (recommended)"), + 2: ("service", "Service (no visual interface)"), + 3: ("ui_control", "UI Control (visual widget)"), +} + + +def create( + # -- Package options -- + extension_type: Optional[str] = typer.Option( + None, + "--type", + "-t", + help="Extension type: [cyan]auto[/cyan], [cyan]service[/cyan] or [cyan]ui_control[/cyan].", + rich_help_panel="Package Options", + ), + flutter_package: Optional[str] = typer.Option( + None, + "--flutter-package", + "-f", + help="Flutter package name from [link=https://pub.dev]pub.dev[/link].", + rich_help_panel="Package Options", + ), + output: Optional[Path] = typer.Option( + None, + "--output", + "-o", + help="Output directory for the generated project.", + rich_help_panel="Package Options", + ), + local_package: Optional[Path] = typer.Option( + None, + "--local-package", + "-l", + help="Path to a local Flutter package (skips pub.dev download).", + rich_help_panel="Package Options", + ), + # -- Code generation options -- + analyze: bool = typer.Option( + True, + "--analyze/--no-analyze", + help=( + "Analyze the Flutter package and auto-generate Python controls, " + "type mappings, and Dart bridge code." + ), + rich_help_panel="Code Generation", + ), + console_module: Optional[bool] = typer.Option( + None, + "--console/--no-console", + help="Include a debug console module for development logging.", + rich_help_panel="Code Generation", + ), + # -- AI refinement options -- + ai_refine: Optional[bool] = typer.Option( + None, + "--ai-refine/--no-ai-refine", + help=( + "Run AI-powered refinement on generated code. " + "An LLM analyzes coverage gaps and applies structured edits " + "using the Architect/Editor pattern. " + "Install: [cyan]uv add flet-pkg\\[ai][/cyan]. " + "Set the API key for your provider (e.g. ANTHROPIC_API_KEY). " + "In interactive mode, you will be prompted if pydantic-ai is installed." + ), + rich_help_panel="AI Refinement", + ), + ai_provider: Optional[str] = typer.Option( + None, + "--ai-provider", + help=( + "AI provider: " + "[cyan]ollama[/cyan] (local, free — no API key), " + "[cyan]anthropic[/cyan] (Claude), " + "[cyan]openai[/cyan] (GPT), or " + "[cyan]google[/cyan] (Gemini). " + "Cloud providers need an API key via env var: " + "ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY. " + "[dim]\\[default: ollama][/dim]" + ), + rich_help_panel="AI Refinement", + ), + ai_model: Optional[str] = typer.Option( + None, + "--ai-model", + help=( + "Override the default model for the selected provider. " + "Defaults: anthropic=[dim]claude-sonnet-4-6[/dim], " + "openai=[dim]gpt-4.1-mini[/dim], " + "google=[dim]gemini-2.5-flash[/dim], " + "ollama=[dim]qwen2.5-coder:14b[/dim]." + ), + rich_help_panel="AI Refinement", + ), + # -- Output options -- + verbose: bool = typer.Option( + False, + "--verbose", + "-v", + help="Show detailed analysis output and coverage breakdown table.", + rich_help_panel="Output", + ), +) -> None: + """[bold]Create a new Flet extension package.[/bold] + + Scaffolds a complete project structure and optionally analyzes the Flutter + package to auto-generate Python controls, Dart bridge code, type mappings, + event handlers, and enum definitions. + + [dim]AI refinement (--ai-refine) uses an LLM to detect and fill coverage + gaps that the deterministic pipeline missed. Requires the \\[ai] extra:[/dim] + + [green]$[/green] uv add flet-pkg\\[ai] + [green]$[/green] ollama pull qwen2.5-coder:14b [dim]# free, local[/dim] + [green]$[/green] flet-pkg create -f shimmer --ai-refine [dim]# uses Ollama[/dim] + + [dim]Or use a cloud provider (requires API key):[/dim] + + [green]$[/green] export ANTHROPIC_API_KEY=sk-... + [green]$[/green] flet-pkg create -f shimmer --ai-refine --ai-provider anthropic + + [dim]Run without flags for interactive mode, or pass flags for CI/scripting.[/dim] + """ + header_panel("Flet Extension Generator", "Create a new Flet extension") + + # Extension type + if extension_type: + template_name = extension_type + if template_name not in ("auto", "service", "ui_control"): + error_panel( + "Error", + f"Invalid type: {template_name}. Use 'auto', 'service' or 'ui_control'.", + ) + raise typer.Exit(1) + else: + choice = ask_choice("Extension type:", [(k, v[1]) for k, v in EXTENSION_TYPES.items()]) + template_name = EXTENSION_TYPES[choice][0] + + # When auto-detect is selected we need the Flutter package name first + # so we can download and inspect it. + if template_name == "auto" and not flutter_package: + flutter_package = ask( + "Flutter package name (from pub.dev)", + validator=validate_flutter_package, + ) + + if template_name == "auto": + assert flutter_package is not None + template_name = _auto_detect_type(flutter_package, local_package) + + # Flutter package (ask if not yet provided) + if not flutter_package: + flutter_package = ask( + "Flutter package name (from pub.dev)", + validator=validate_flutter_package, + ) + + derived = derive_names(flutter_package) + + # Project name + project_name = ask( + "Extension name", + default=derived.project_name, + validator=validate_project_name, + ) + + # Check PyPI / GitHub for existing packages + _check_existing_packages(project_name) + + # Package name + package_name = ask( + "Python package name", + default=derived.package_name, + validator=validate_package_name, + ) + + # Control class name + control_name = ask( + "Control class name", + default=derived.control_name, + validator=validate_control_name, + ) + + # Description + description = ask("Description", default="A Flet extension package.") + + # Author + author = ask("Author", default="") + + # Debug console module + if console_module is None: + console_module = ask_confirm("Include debug console module?", default=True) + + # AI refinement + if ai_refine is None and analyze: + ai_refine = _ask_ai_refine() + if ai_refine and ai_provider is None: + ai_provider, detected_model = _ask_ai_provider() + if detected_model and ai_model is None: + ai_model = detected_model + if ai_refine is None: + ai_refine = False + + # Fetch Flutter package version for pubspec.yaml + flutter_package_version = "any" + if flutter_package and not local_package: + try: + from flet_pkg.core.downloader import PubDevDownloader + + metadata = PubDevDownloader().fetch_metadata(flutter_package) + if metadata.version: + major, minor, *_ = metadata.version.split(".") + flutter_package_version = f"^{major}.{minor}.0" + except Exception: + pass + + context = { + "project_name": project_name, + "package_name": package_name, + "control_name": control_name, + "control_name_snake": derived.control_name_snake, + "flutter_package": flutter_package, + "flutter_package_version": flutter_package_version, + "description": description, + "author": author, + "include_console": console_module, + } + + output_dir = output or Path.cwd() + + try: + scaffolder = Scaffolder(template_name, context, output_dir) + project_dir = scaffolder.generate() + except FileExistsError as e: + error_panel("Error", str(e)) + raise typer.Exit(1) + except Exception as e: + error_panel("Error", f"Failed to generate project: {e}") + raise typer.Exit(1) + + # Remove console.py if --no-console + if not console_module: + console_file = project_dir / "src" / package_name / "console.py" + if console_file.exists(): + console_file.unlink() + + # Run analysis pipeline if --analyze + if analyze: + console.print() + console.print("[info]Analyzing Flutter package...[/info]") + + try: + from flet_pkg.core.pipeline import GenerationPipeline + + pipeline = GenerationPipeline() + result = pipeline.run( + flutter_package=flutter_package, + control_name=control_name, + extension_type=template_name, + project_dir=project_dir, + package_name=package_name, + description=description, + local_package=local_package, + control_name_snake=context["control_name_snake"], + include_console=console_module, + ai_refine=ai_refine, + ai_provider=ai_provider, + ai_model=ai_model, + verbose=verbose, + ) + + if result.warnings: + for warning in result.warnings: + console.print(f" [warning]{warning}[/warning]") + + if result.files_generated: + console.print( + f"\n[success]Auto-generated {len(result.files_generated)} files " + f"from {flutter_package} analysis.[/success]" + ) + + # Coverage score (always shown when available) + if result.gap_report is not None: + from flet_pkg.ui.coverage import print_coverage_score, print_coverage_table + + print_coverage_score( + result.coverage_pct, + result.gap_report.total_generated, + result.gap_report.total_dart_api, + ai_pct=result.ai_coverage_pct, + ) + if verbose: + print_coverage_table(result.gap_report) + except Exception as e: + console.print(f" [warning]Analysis skipped: {e}[/warning]") + + print_tree(project_dir) + + console.print() + info_panel( + "Next Steps", + f"cd {project_name}\nuv sync", + ) + + +def _auto_detect_type( + flutter_package: str, + local_package: Path | None = None, +) -> str: + """Download (or locate) the Flutter package and detect extension type. + + Falls back to a manual choice prompt if the download or detection fails. + + Args: + flutter_package: Name of the Flutter package on pub.dev. + local_package: Optional local path to the Flutter package. + + Returns: + Extension type string: ``"ui_control"`` or ``"service"``. + """ + from flet_pkg.core.downloader import PubDevDownloader + from flet_pkg.core.parser import detect_extension_type + + try: + if local_package: + package_path = local_package + else: + downloader = PubDevDownloader() + package_path = downloader.download(flutter_package) + + detected = detect_extension_type(package_path) + label = ( + "UI Control (visual widget)" + if detected == "ui_control" + else "Service (no visual interface)" + ) + console.print(f"\n [success]Detected: {label}[/success]\n") + return detected + except Exception as e: + console.print(f"\n [warning]Auto-detect failed ({e}). Please choose manually:[/warning]") + manual_choices = [ + (1, "Service (no visual interface)"), + (2, "UI Control (visual widget)"), + ] + choice = ask_choice("Extension type:", manual_choices) + return "service" if choice == 1 else "ui_control" + + +def _check_existing_packages(project_name: str) -> None: + """Check PyPI and GitHub for existing packages with the same name. + + Shows warnings and asks for confirmation when matches are found. + Silently continues on network errors. + """ + matches: list[RegistryMatch] = [] + + with console.status("[info]Checking PyPI, GitHub and Flet SDK...[/info]"): + pypi_match = check_pypi(project_name) + if pypi_match: + matches.append(pypi_match) + + flet_match = check_flet_packages(project_name) + if flet_match: + matches.append(flet_match) + + gh_matches = check_github(project_name) + matches.extend(gh_matches) + + if not matches: + return + + console.print(f"\n [warning]Found existing packages matching '{project_name}':[/warning]") + for m in matches: + desc = f" — {m.description}" if m.description else "" + console.print(f" [highlight]{m.source}[/highlight]: {m.name}{desc}") + console.print(f" {m.url}") + + console.print() + if not ask_confirm("Continue anyway?"): + raise typer.Exit(0) + + +AI_PROVIDERS = { + 1: ("ollama", "Ollama (local, free — no API key)"), + 2: ("anthropic", "Anthropic (Claude — requires API key)"), + 3: ("openai", "OpenAI (GPT — requires API key)"), + 4: ("google", "Google (Gemini — requires API key)"), +} + + +def _ask_ai_refine() -> bool: + """Ask the user whether to enable AI refinement. + + Returns: + ``True`` if the user opts in and ``pydantic_ai`` is installed. + """ + try: + import pydantic_ai # noqa: F401 + + return ask_confirm("Run AI refinement on generated code?", default=False) + except ImportError: + return False + + +def _ask_ai_provider() -> tuple[str, str | None]: + """Ask the user to choose an AI provider. + + Returns: + Tuple of (provider, model). *model* is ``None`` unless the user + picked a specific Ollama model interactively. + """ + choice = ask_choice("AI provider:", [(k, v[1]) for k, v in AI_PROVIDERS.items()]) + provider = AI_PROVIDERS[choice][0] + model: str | None = None + + if provider == "ollama": + model = _detect_ollama_model() + else: + from flet_pkg.core.ai.config import AIConfig + + config = AIConfig.load(provider=provider) + if not config.is_available(): + import os + + env_var = {"anthropic": "ANTHROPIC_API_KEY", "openai": "OPENAI_API_KEY"}.get( + provider, "GOOGLE_API_KEY" + ) + current = os.environ.get(env_var, "") + if not current: + console.print( + f"\n [warning]No {env_var} found. " + f"Set it before running or AI will be skipped.[/warning]" + ) + + return provider, model + + +def _detect_ollama_model() -> str | None: + """Detect installed Ollama models and let the user pick one. + + Returns: + Selected model name, or ``None`` to use the default. + """ + from flet_pkg.core.ai.config import AIConfig, list_ollama_models + + config = AIConfig.load(provider="ollama") + + with console.status("[info]Detecting Ollama models...[/info]", spinner="dots"): + models = list_ollama_models() + + if not models: + console.print( + f"\n [warning]Could not connect to Ollama or no models installed.[/warning]\n" + f" [dim]Make sure Ollama is running: ollama serve[/dim]\n" + f" [dim]Pull a model first: ollama pull {config.model}[/dim]" + ) + return None + + if len(models) == 1: + selected = models[0] + console.print(f"\n [success]Ollama model detected: {selected}[/success]") + return selected + + # Multiple models — let the user choose + console.print(f"\n [info]Found {len(models)} Ollama models:[/info]") + model_choices = [(i + 1, m) for i, m in enumerate(models)] + idx = ask_choice("Select model:", model_choices) + selected = models[idx - 1] + return selected diff --git a/src/flet_pkg/core/__init__.py b/src/flet_pkg/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/flet_pkg/core/ai/__init__.py b/src/flet_pkg/core/ai/__init__.py new file mode 100644 index 0000000..ed84f49 --- /dev/null +++ b/src/flet_pkg/core/ai/__init__.py @@ -0,0 +1,12 @@ +"""AI-powered refinement for generated Flet extension code. + +This optional module uses the Architect/Editor pattern (inspired by Aider) +to analyze coverage gaps and apply LLM-driven improvements to generated code. + +Requires: ``uv add flet-pkg[ai]`` (pydantic-ai). +""" + +from flet_pkg.core.ai.config import AIConfig +from flet_pkg.core.ai.models import GapReport, RefinementResult + +__all__ = ["AIConfig", "GapReport", "RefinementResult"] diff --git a/src/flet_pkg/core/ai/agent.py b/src/flet_pkg/core/ai/agent.py new file mode 100644 index 0000000..9452340 --- /dev/null +++ b/src/flet_pkg/core/ai/agent.py @@ -0,0 +1,195 @@ +"""Architect and Editor agents using pydantic-ai. + +Two-agent pattern: +- **Architect**: Reasons about gaps, produces improvement plan (structured output) +- **Editor**: Produces precise search/replace edits (structured output) + +All pydantic-ai imports are lazy — this module can be imported but will only +work when pydantic-ai is installed. +""" + +from dataclasses import dataclass, field +from typing import Any + +from flet_pkg.core.ai.models import ( + ArchitectPlan, + EditorResult, + FileEdit, + GapReport, +) +from flet_pkg.core.ai.prompts import ARCHITECT_SYSTEM_PROMPT, EDITOR_SYSTEM_PROMPT + +# --------------------------------------------------------------------------- +# Dependency containers (passed to agents at run time) +# --------------------------------------------------------------------------- + + +@dataclass +class ArchitectDeps: + """Dependencies available to the Architect agent via tools.""" + + gap_report: GapReport + dart_sources: dict[str, str] = field(default_factory=dict) + generated_files: dict[str, str] = field(default_factory=dict) + + +@dataclass +class EditorDeps: + """Dependencies available to the Editor agent via tools.""" + + architect_plan: ArchitectPlan + file_contents: dict[str, str] = field(default_factory=dict) + feedback: str = "" + + +# --------------------------------------------------------------------------- +# Agent builders (lazy — only call when pydantic-ai is installed) +# --------------------------------------------------------------------------- + + +def build_architect_agent(model: Any) -> Any: + """Build the Architect agent with tools for gap inspection. + + Args: + model: A pydantic-ai model instance. + + Returns: + A pydantic-ai Agent configured for the Architect role. + """ + from pydantic_ai import Agent, RunContext + + agent = Agent( + model, + deps_type=ArchitectDeps, + output_type=ArchitectPlan, + system_prompt=ARCHITECT_SYSTEM_PROMPT, + retries=3, + ) + + @agent.tool + def get_gap_report(ctx: RunContext[ArchitectDeps]) -> str: + """Get the full gap report as a formatted summary.""" + report = ctx.deps.gap_report + lines = [report.summary(), "", "Gaps:"] + for i, gap in enumerate(report.gaps): + feasible = "feasible" if gap.feasible else "INFEASIBLE" + lines.append( + f" [{i}] {gap.kind.value}: {gap.dart_name} " + f"(type={gap.dart_type}, class={gap.dart_class}) " + f"[{feasible}] — {gap.reason}" + ) + return "\n".join(lines) + + @agent.tool + def get_dart_source_snippet(ctx: RunContext[ArchitectDeps], class_name: str) -> str: + """Get Dart source code for a specific class (first 200 lines).""" + for filename, content in ctx.deps.dart_sources.items(): + if class_name in content: + lines = content.split("\n")[:200] + return f"// {filename}\n" + "\n".join(lines) + return f"No Dart source found containing class '{class_name}'" + + @agent.tool + def get_generated_file_summary(ctx: RunContext[ArchitectDeps]) -> str: + """Get a summary of all generated files (names and line counts).""" + lines = ["Generated files:"] + for filename, content in ctx.deps.generated_files.items(): + n_lines = content.count("\n") + 1 + lines.append(f" {filename}: {n_lines} lines") + return "\n".join(lines) + + return agent + + +def build_editor_agent(model: Any) -> Any: + """Build the Editor agent with tools for file inspection. + + Args: + model: A pydantic-ai model instance. + + Returns: + A pydantic-ai Agent configured for the Editor role. + """ + from pydantic_ai import Agent, RunContext + + agent = Agent( + model, + deps_type=EditorDeps, + output_type=EditorResult, + system_prompt=EDITOR_SYSTEM_PROMPT, + retries=3, + ) + + @agent.tool + def get_file_content(ctx: RunContext[EditorDeps], filename: str) -> str: + """Get the full content of a generated file.""" + content = ctx.deps.file_contents.get(filename) + if content is None: + available = ", ".join(ctx.deps.file_contents.keys()) + return f"File '{filename}' not found. Available: {available}" + return content + + @agent.tool + def list_files(ctx: RunContext[EditorDeps]) -> str: + """List all available generated files.""" + return "\n".join(ctx.deps.file_contents.keys()) + + return agent + + +# --------------------------------------------------------------------------- +# Edit application + validation +# --------------------------------------------------------------------------- + + +def apply_edits( + edits: list[FileEdit], + files: dict[str, str], +) -> tuple[dict[str, str], int, int]: + """Apply search/replace edits to file contents. + + Args: + edits: List of FileEdit objects with search/replace strings. + files: Dict mapping filename to file content. + + Returns: + Tuple of (modified_files, applied_count, failed_count). + """ + modified = dict(files) + applied = 0 + failed = 0 + + for edit in edits: + if edit.filename not in modified: + failed += 1 + continue + + content = modified[edit.filename] + if edit.search not in content: + failed += 1 + continue + + modified[edit.filename] = content.replace(edit.search, edit.replace, 1) + applied += 1 + + return modified, applied, failed + + +def validate_python_syntax(files: dict[str, str]) -> list[str]: + """Validate Python files for syntax errors. + + Args: + files: Dict mapping filename to file content. + + Returns: + List of error messages (empty if all valid). + """ + errors: list[str] = [] + for filename, content in files.items(): + if not filename.endswith(".py"): + continue + try: + compile(content, filename, "exec") + except SyntaxError as e: + errors.append(f"{filename}:{e.lineno}: {e.msg}") + return errors diff --git a/src/flet_pkg/core/ai/config.py b/src/flet_pkg/core/ai/config.py new file mode 100644 index 0000000..8b60448 --- /dev/null +++ b/src/flet_pkg/core/ai/config.py @@ -0,0 +1,103 @@ +"""AI refinement configuration. + +Loads settings from CLI flags, environment variables, or provider defaults. +Priority: CLI flags > env vars > provider defaults. +""" + +from __future__ import annotations + +import os +from dataclasses import dataclass + +# Provider → (default model, env var for API key) +_PROVIDER_DEFAULTS: dict[str, tuple[str, str]] = { + "anthropic": ("claude-sonnet-4-6", "ANTHROPIC_API_KEY"), + "openai": ("gpt-4.1-mini", "OPENAI_API_KEY"), + "google": ("gemini-2.5-flash", "GOOGLE_API_KEY"), + "ollama": ("qwen2.5-coder:14b", ""), +} + + +@dataclass +class AIConfig: + """Configuration for the AI refinement pipeline.""" + + provider: str = "ollama" + model: str = "" + api_key: str = "" + base_url: str = "" + temperature: float = 0.1 + max_tokens: int = 4096 + + @classmethod + def load( + cls, + provider: str | None = None, + model: str | None = None, + ) -> AIConfig: + """Load configuration from CLI flags and environment variables. + + Args: + provider: Provider name (anthropic, openai, google, ollama). + model: Model name override. + + Returns: + Populated AIConfig instance. + """ + # Determine provider + resolved_provider = provider or os.environ.get("FLET_PKG_AI_PROVIDER", "ollama") + if resolved_provider not in _PROVIDER_DEFAULTS: + resolved_provider = "ollama" + + default_model, env_key = _PROVIDER_DEFAULTS[resolved_provider] + + # Determine model + resolved_model = model or os.environ.get("FLET_PKG_AI_MODEL", "") or default_model + + # Determine API key + api_key = "" + if env_key: + api_key = os.environ.get(env_key, "") + + # Ollama base URL + base_url = "" + if resolved_provider == "ollama": + base_url = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434/v1") + + return cls( + provider=resolved_provider, + model=resolved_model, + api_key=api_key, + base_url=base_url, + ) + + def is_available(self) -> bool: + """Check if the provider is configured and usable. + + Returns: + ``True`` if the provider can be used (Ollama always, others need API key). + """ + if self.provider == "ollama": + return True + return bool(self.api_key) + + +def list_ollama_models() -> list[str]: + """Query the Ollama API for locally installed models. + + Returns a list of model names (e.g. ``["qwen2.5-coder:7b", "llama3:8b"]``). + Returns an empty list if Ollama is unreachable. + """ + import httpx + + base = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434") + # Strip /v1 suffix if present (the tags endpoint is on the base URL) + base = base.removesuffix("/v1") + + try: + resp = httpx.get(f"{base}/api/tags", timeout=5.0) + resp.raise_for_status() + data = resp.json() + return [m["name"] for m in data.get("models", [])] + except Exception: + return [] diff --git a/src/flet_pkg/core/ai/gap_analyzer.py b/src/flet_pkg/core/ai/gap_analyzer.py new file mode 100644 index 0000000..6a9bbac --- /dev/null +++ b/src/flet_pkg/core/ai/gap_analyzer.py @@ -0,0 +1,479 @@ +"""Deterministic coverage gap analyzer. + +Compares ``DartPackageAPI`` (source) against ``GenerationPlan`` (output) +to identify exactly what the pipeline missed and why. Produces a structured +``GapReport`` — no LLM required. +""" + +import re + +from flet_pkg.core.ai.models import GapItem, GapKind, GapReport +from flet_pkg.core.models import DartPackageAPI, GenerationPlan +from flet_pkg.core.parser import camel_to_snake +from flet_pkg.core.type_map import map_dart_type, map_dart_type_flet + +# Internal Dart methods excluded from gap counting. +_INTERNAL_METHOD_NAMES = frozenset( + { + "setMockInitialValues", + "setMockMethodCallHandler", + "hashCode", + "getHashCode", + "runtimeType", + "toString", + "noSuchMethod", + "toJson", + "fromJson", + "toMap", + "fromMap", + "copyWith", + "compareTo", + "toJsonString", + "fromJsonFunction", + "getFromJsonFunction", + "removeDuplicates", + "addIfNotNull", + "compute", + "identical", + "jsonRepresentation", + "convertToJsonString", + "jsonEncode", + # Dart control-flow keywords that the parser may capture as methods + "catch", + "catch_", + "finally", + "finally_", + "then", + "then_", + "rethrow", + "rethrow_", + } +) + +# Framework constructor params that are never mapped. +_FRAMEWORK_PARAMS = frozenset({"key", "child", "children"}) + +# Dart types that cannot be serialized to Flet properties. +_NON_SERIALIZABLE_TYPES = frozenset( + { + "LinearGradient", + "Gradient", + "MaskFilter", + "Curves", + "ScrollPhysics", + "NeverScrollableScrollPhysics", + "AlwaysScrollableScrollPhysics", + "EdgeInsetsGeometry", + "EdgeInsets", + "BorderRadius", + "BorderSide", + "BoxDecoration", + "ShapeBorder", + "BoxBorder", + "AnimationController", + "ScrollController", + "TextEditingController", + "MouseCursor", + "SystemMouseCursors", + "HitTestBehavior", + } +) + +# Callback types that become events (not gaps). +_CALLBACK_TYPES = frozenset( + { + "VoidCallback", + "GestureTapCallback", + "GestureLongPressCallback", + } +) +_CALLBACK_PREFIXES = ( + "ValueChanged", + "ValueSetter", + "ValueGetter", + "IndexedWidgetBuilder", + "WidgetBuilder", + "NullableIndexedWidgetBuilder", +) + +# Regex for add/remove listener/observer/handler patterns. +_LISTENER_RE = re.compile(r"^(add|remove)(.*?)(Listener|Observer|Handler|Subscription)s?$") + +# Methods that map to constructor properties (SDK init flow). +_INIT_METHOD_NAMES = frozenset({"initialize", "init", "setup", "configure"}) + + +def _is_listener_method(name: str) -> bool: + """Return True if *name* is an add/remove listener/observer/handler.""" + return _LISTENER_RE.match(name) is not None + + +# Verb prefixes that the analyzer considers as already meaningful — +# bare getters without these get a ``get_`` or ``is_`` prefix added. +_VERB_PREFIXES = ("get_", "is_", "are_", "can_", "has_", "should_", "does_", "will_", "was_") + + +def _bare_getter_name(method, python_name: str) -> str: + """Return the prefixed name the analyzer would generate for a bare getter. + + Mirrors the ``_normalize_method_name`` logic in ``analyzer.py``: bare + getters (no params, non-void return, no existing verb prefix) get a + ``get_`` or ``is_`` prefix. + """ + if ( + method.is_getter + and not method.params + and method.return_type not in ("void", "Future") + and not any(python_name.startswith(vp) for vp in _VERB_PREFIXES) + ): + is_bool = method.return_type in ("bool", "bool?", "Future", "Future") + return f"{'is_' if is_bool else 'get_'}{python_name}" + return python_name + + +class GapAnalyzer: + """Deterministic gap analysis between Dart source and generated plan.""" + + def analyze( + self, + api: DartPackageAPI, + plan: GenerationPlan, + extension_type: str, + ) -> GapReport: + """Analyze gaps between Dart API surface and generated plan. + + Args: + api: Parsed Dart package API. + plan: Generated plan to compare against. + extension_type: Either ``"service"`` or ``"ui_control"``. + + Returns: + A ``GapReport`` with coverage stats and missing items. + """ + if extension_type == "service": + return self._analyze_service_gaps(api, plan) + return self._analyze_ui_control_gaps(api, plan) + + def _analyze_service_gaps( + self, + api: DartPackageAPI, + plan: GenerationPlan, + ) -> GapReport: + """Analyze gaps for a service (non-visual) package.""" + report = GapReport( + flutter_package=plan.flutter_package, + extension_type="service", + ) + + # Per-category counters: [dart_api, generated] + cat: dict[str, list[int]] = { + "Methods": [0, 0], + "Events": [0, 0], + "Enums": [0, 0], + } + + # Collect all generated method names (main + sub-modules) + generated_methods: set[str] = set() + for m in plan.main_methods: + generated_methods.add(m.python_name) + for sub in plan.sub_modules: + for m in sub.methods: + generated_methods.add(m.python_name) + + # Collect generated event names + generated_events: set[str] = set() + for e in plan.events: + generated_events.add(e.python_attr_name) + + # Collect generated enum names + generated_enums: set[str] = set() + for e in plan.enums: + generated_enums.add(e.python_name) + + # Collect generated property names + generated_props: set[str] = set() + for p in plan.properties: + generated_props.add(p.python_name) + + # Analyze class methods + for cls in api.classes: + for method in cls.methods: + if method.name in _INTERNAL_METHOD_NAMES: + continue + + # Stream getters → events + if method.is_getter and method.return_type.startswith("Stream"): + report.total_dart_api += 1 + cat["Events"][0] += 1 + event_name = f"on_{camel_to_snake(method.name)}" + if not any( + e.python_attr_name == event_name + or e.python_attr_name + == f"on_{camel_to_snake(method.name).removeprefix('on_')}" + for e in plan.events + ): + report.gaps.append( + GapItem( + kind=GapKind.MISSING_EVENT, + dart_name=method.name, + dart_type=method.return_type, + dart_class=cls.name, + reason="Stream getter not mapped to event handler", + ) + ) + else: + report.total_generated += 1 + cat["Events"][1] += 1 + continue + + # add/remove Listener/Observer/Handler → covered by events + if _is_listener_method(method.name) and generated_events: + report.total_dart_api += 1 + cat["Events"][0] += 1 + report.total_generated += 1 + cat["Events"][1] += 1 + continue + + # Bare getters/setters mapped as properties + python_name = camel_to_snake(method.name) + if (method.is_getter or method.is_setter) and python_name in generated_props: + report.total_dart_api += 1 + cat["Methods"][0] += 1 + report.total_generated += 1 + cat["Methods"][1] += 1 + continue + + # Bare getter with get_/is_ prefix (analyzer adds prefix for + # unnamed getters with non-void return and no verb prefix) + prefixed_name = _bare_getter_name(method, python_name) + + # Init/setup methods covered by constructor properties + if method.name in _INIT_METHOD_NAMES and generated_props: + report.total_dart_api += 1 + cat["Methods"][0] += 1 + report.total_generated += 1 + cat["Methods"][1] += 1 + continue + + # Regular methods + report.total_dart_api += 1 + cat["Methods"][0] += 1 + if python_name in generated_methods or prefixed_name in generated_methods: + report.total_generated += 1 + cat["Methods"][1] += 1 + else: + report.gaps.append( + GapItem( + kind=GapKind.MISSING_METHOD, + dart_name=method.name, + dart_type=method.return_type, + dart_class=cls.name, + reason="Method not mapped to Python async method", + feasible=self._is_method_feasible(method.return_type), + ) + ) + + # Top-level functions + for func in api.top_level_functions: + report.total_dart_api += 1 + cat["Methods"][0] += 1 + python_name = camel_to_snake(func.name) + if python_name in generated_methods: + report.total_generated += 1 + cat["Methods"][1] += 1 + else: + report.gaps.append( + GapItem( + kind=GapKind.MISSING_METHOD, + dart_name=func.name, + dart_type=func.return_type, + reason="Top-level function not mapped", + feasible=self._is_method_feasible(func.return_type), + ) + ) + + # Enum gaps + for dart_enum in api.enums: + report.total_dart_api += 1 + cat["Enums"][0] += 1 + if dart_enum.name in generated_enums or any( + e.python_name == dart_enum.name for e in plan.enums + ): + report.total_generated += 1 + cat["Enums"][1] += 1 + else: + report.gaps.append( + GapItem( + kind=GapKind.MISSING_ENUM, + dart_name=dart_enum.name, + reason="Dart enum not mapped to Python Enum", + ) + ) + + # Coverage + if report.total_dart_api > 0: + report.coverage_pct = report.total_generated / report.total_dart_api * 100 + + # Store per-category counts (only non-empty categories) + report.category_counts = {k: (v[0], v[1]) for k, v in cat.items() if v[0] > 0} + + return report + + def _analyze_ui_control_gaps( + self, + api: DartPackageAPI, + plan: GenerationPlan, + ) -> GapReport: + """Analyze gaps for a UI control (visual widget) package.""" + report = GapReport( + flutter_package=plan.flutter_package, + extension_type="ui_control", + ) + + # Per-category counters: [dart_api, generated] + cat: dict[str, list[int]] = { + "Properties": [0, 0], + "Events": [0, 0], + "Enums": [0, 0], + } + + # Find the main widget class + widget_classes = [c for c in api.classes if c.constructor_params] + main_widget = next( + (c for c in widget_classes if c.name == plan.dart_main_class), + widget_classes[0] if widget_classes else None, + ) + + if not main_widget: + return report + + # Collect generated property/event names + generated_props: set[str] = {p.python_name for p in plan.properties} + generated_events: set[str] = {e.python_attr_name for e in plan.events} + generated_enums: set[str] = {e.python_name for e in plan.enums} + + # Sub-control property names + sub_control_props: set[str] = set() + for sc in plan.sub_controls: + sub_control_props.add(camel_to_snake(sc.dart_class_name)) + for p in sc.properties: + sub_control_props.add(p.python_name) + + # Analyze constructor params + for param in main_widget.constructor_params: + if param.name in _FRAMEWORK_PARAMS: + continue + + report.total_dart_api += 1 + python_name = camel_to_snake(param.name) + + # Check if mapped as property, event, or sub-control + is_callback = self._is_callback_type(param.dart_type) + + if is_callback: + cat["Events"][0] += 1 + event_name = f"on_{python_name.removeprefix('on_')}" + if event_name in generated_events or python_name in generated_events: + report.total_generated += 1 + cat["Events"][1] += 1 + else: + report.gaps.append( + GapItem( + kind=GapKind.MISSING_EVENT, + dart_name=param.name, + dart_type=param.dart_type, + dart_class=main_widget.name, + reason="Callback param not mapped to event handler", + ) + ) + elif python_name in generated_props or python_name in sub_control_props: + cat["Properties"][0] += 1 + report.total_generated += 1 + cat["Properties"][1] += 1 + else: + cat["Properties"][0] += 1 + feasible = self._is_property_feasible(param.dart_type) + report.gaps.append( + GapItem( + kind=GapKind.MISSING_PROPERTY, + dart_name=param.name, + dart_type=param.dart_type, + dart_class=main_widget.name, + reason=self._property_skip_reason(param.dart_type), + feasible=feasible, + ) + ) + + # Enum gaps + for dart_enum in api.enums: + report.total_dart_api += 1 + cat["Enums"][0] += 1 + if dart_enum.name in generated_enums or any( + e.python_name == dart_enum.name for e in plan.enums + ): + report.total_generated += 1 + cat["Enums"][1] += 1 + else: + report.gaps.append( + GapItem( + kind=GapKind.MISSING_ENUM, + dart_name=dart_enum.name, + reason="Dart enum not mapped to Python Enum", + ) + ) + + # Coverage + if report.total_dart_api > 0: + report.coverage_pct = report.total_generated / report.total_dart_api * 100 + + # Store per-category counts (only non-empty categories) + report.category_counts = {k: (v[0], v[1]) for k, v in cat.items() if v[0] > 0} + + return report + + # ------------------------------------------------------------------ + # Feasibility helpers + # ------------------------------------------------------------------ + + @staticmethod + def _is_method_feasible(return_type: str) -> bool: + """Check if a method with this return type is feasibly mappable.""" + base = return_type.replace("?", "").strip() + if base.startswith("Future<"): + base = base[7:-1].strip() + if base.startswith("Stream<"): + return True + mapped = map_dart_type(base) + return mapped != base or base in ("void", "dynamic") + + @staticmethod + def _is_callback_type(dart_type: str) -> bool: + """Check if a Dart type is a callback/function type.""" + base = dart_type.replace("?", "").strip() + if base in _CALLBACK_TYPES: + return True + if any(base.startswith(prefix) for prefix in _CALLBACK_PREFIXES): + return True + if "Function" in base or "Callback" in base: + return True + return False + + @staticmethod + def _is_property_feasible(dart_type: str) -> bool: + """Check if a Dart type can feasibly become a Flet property.""" + base = dart_type.replace("?", "").strip() + if base in _NON_SERIALIZABLE_TYPES: + return False + mapped = map_dart_type_flet(base) + return mapped is not None + + @staticmethod + def _property_skip_reason(dart_type: str) -> str: + """Return human-readable reason why a property was skipped.""" + base = dart_type.replace("?", "").strip() + if base in _NON_SERIALIZABLE_TYPES: + return f"Non-serializable type: {base}" + mapped = map_dart_type_flet(base) + if mapped is None: + return f"Type skipped by Flet mapping: {base}" + return f"Unknown type mapping: {base}" diff --git a/src/flet_pkg/core/ai/models.py b/src/flet_pkg/core/ai/models.py new file mode 100644 index 0000000..9c6bf08 --- /dev/null +++ b/src/flet_pkg/core/ai/models.py @@ -0,0 +1,133 @@ +"""Data models for the AI refinement pipeline. + +Uses Pydantic BaseModel for structured LLM output validation. +All models are importable without pydantic-ai installed (only pydantic needed +at runtime when AI features are used). +""" + +from dataclasses import dataclass, field +from enum import Enum + +# --------------------------------------------------------------------------- +# Gap Report (deterministic, Step 1 — no LLM) +# --------------------------------------------------------------------------- + + +class GapKind(str, Enum): + """Classification of a coverage gap.""" + + MISSING_METHOD = "missing_method" + MISSING_PROPERTY = "missing_property" + MISSING_ENUM = "missing_enum" + MISSING_EVENT = "missing_event" + TYPE_MISMATCH = "type_mismatch" + INCOMPLETE_STUB = "incomplete_stub" + + +@dataclass +class GapItem: + """A single coverage gap between Dart source and generated code.""" + + kind: GapKind + dart_name: str + dart_type: str = "" + dart_class: str = "" + reason: str = "" + feasible: bool = True + context: str = "" + + +@dataclass +class GapReport: + """Structured report of coverage gaps (deterministic output).""" + + flutter_package: str + extension_type: str + total_dart_api: int = 0 + total_generated: int = 0 + coverage_pct: float = 0.0 + gaps: list[GapItem] = field(default_factory=list) + category_counts: dict[str, tuple[int, int]] = field(default_factory=dict) + """Per-category (dart_api_count, generated_count) — e.g. {"Methods": (25, 24)}.""" + + @property + def feasible_gaps(self) -> int: + return sum(1 for g in self.gaps if g.feasible) + + def summary(self) -> str: + lines = [ + f"Package: {self.flutter_package} ({self.extension_type})", + f"Coverage: {self.total_generated}/{self.total_dart_api} ({self.coverage_pct:.1f}%)", + f"Gaps: {len(self.gaps)} total, {self.feasible_gaps} feasible", + ] + return "\n".join(lines) + + +# --------------------------------------------------------------------------- +# Architect Output (Step 2 — LLM structured output) +# --------------------------------------------------------------------------- + + +@dataclass +class ImprovementSuggestion: + """A single improvement suggested by the Architect.""" + + target_file: str + description: str + gap_refs: list[int] = field(default_factory=list) + priority: int = 1 + + +@dataclass +class ArchitectPlan: + """The Architect's analysis and improvement plan.""" + + analysis: str + suggestions: list[ImprovementSuggestion] = field(default_factory=list) + files_to_skip: list[str] = field(default_factory=list) + + +# --------------------------------------------------------------------------- +# Editor Output (Step 3 — LLM structured output) +# --------------------------------------------------------------------------- + + +@dataclass +class FileEdit: + """A precise search/replace edit to apply to a file.""" + + filename: str + search: str + replace: str + rationale: str = "" + + +@dataclass +class EditorResult: + """Collection of edits from the Editor agent.""" + + edits: list[FileEdit] = field(default_factory=list) + summary: str = "" + + +# --------------------------------------------------------------------------- +# Final Result +# --------------------------------------------------------------------------- + + +@dataclass +class RefinementResult: + """Final output of the AI refinement pipeline.""" + + gap_report: GapReport + architect_plan: ArchitectPlan | None = None + edits_applied: int = 0 + edits_failed: int = 0 + validation_passed: bool = False + overall_assessment: str = "" + input_tokens: int = 0 + output_tokens: int = 0 + file_diffs: list[tuple[str, str]] = field(default_factory=list) + """(filename, unified_diff_text) for files modified by AI.""" + pending_suggestions: list[ImprovementSuggestion] = field(default_factory=list) + """Architect suggestions that the Editor did not apply.""" diff --git a/src/flet_pkg/core/ai/prompts.py b/src/flet_pkg/core/ai/prompts.py new file mode 100644 index 0000000..85a4a2a --- /dev/null +++ b/src/flet_pkg/core/ai/prompts.py @@ -0,0 +1,50 @@ +"""System prompts for the Architect and Editor LLM agents. + +Separates reasoning (Architect) from editing (Editor) following +the Architect/Editor pattern from Aider research. +""" + +ARCHITECT_SYSTEM_PROMPT = """\ +You are a Flet extension code architect. You analyze coverage gaps between +a Flutter/Dart package and its generated Python/Flet extension code. + +You receive a gap report showing what the code generation pipeline missed. +Your job is to analyze each gap and describe HOW to fix it. + +Focus on: +- Missing methods that should map to Python async methods with invoke_method() +- Missing properties that should map to dataclass fields or ft.Control properties +- Type corrections (wrong Python type for a Dart type) +- Missing enum values that should be added to existing Python Enum classes +- Missing event handlers for Dart stream getters or callback parameters + +Rules: +1. Do NOT produce code edits — only describe what needs to change and in which file +2. Prioritize feasible gaps (where the Dart type has a clear Python mapping) +3. Skip gaps where the Dart type is non-serializable (ScrollController, AnimationController, etc.) +4. Follow existing code patterns — if the file uses invoke_method(), new methods should too +5. Keep suggestions minimal — only add what's actually useful for the Python/Flet developer +6. For enum gaps, suggest adding values to the existing enum class in types.py +7. For method gaps, describe the Python method signature, return type, and invoke key +""" + +EDITOR_SYSTEM_PROMPT = """\ +You are a precise code editor for Flet extension packages. You receive +improvement descriptions from an architect and the actual file contents. +Your job is to produce exact search/replace edits. + +Rules: +1. The search string must be an EXACT substring of the current file content +2. Never remove existing functionality — only add or modify +3. Follow the existing code style exactly: + - 4-space indentation + - Double quotes for strings + - Type hints on all parameters and return types + - Docstrings on public methods +4. Keep edits minimal — change only what's needed for the improvement +5. For new methods, place them in logical order near similar methods +6. For new enum values, add them at the end of the existing enum class +7. Ensure all imports are added if new types are referenced +8. Use invoke_method() for async service calls, matching existing patterns +9. Use field(default=...) for new dataclass/control properties +""" diff --git a/src/flet_pkg/core/ai/provider.py b/src/flet_pkg/core/ai/provider.py new file mode 100644 index 0000000..40b395c --- /dev/null +++ b/src/flet_pkg/core/ai/provider.py @@ -0,0 +1,55 @@ +"""LLM provider factory with lazy imports. + +Creates pydantic-ai model instances from AIConfig. All pydantic-ai imports +are deferred so that non-AI users are never affected by missing dependencies. +""" + +from typing import Any + +from flet_pkg.core.ai.config import AIConfig + + +def create_model(config: AIConfig) -> Any: + """Create a pydantic-ai model from the given configuration. + + Args: + config: AI configuration with provider, model, and API key. + + Returns: + A pydantic-ai model instance. + + Raises: + ImportError: If pydantic-ai is not installed. + ValueError: If the provider is not supported. + """ + try: + from pydantic_ai.models.anthropic import AnthropicModel + from pydantic_ai.models.openai import OpenAIChatModel + except ImportError: + raise ImportError("AI refinement requires pydantic-ai. Install with: uv add flet-pkg[ai]") + + if config.provider == "anthropic": + return AnthropicModel(config.model) + + if config.provider == "openai": + return OpenAIChatModel(config.model) + + if config.provider == "google": + try: + from pydantic_ai.models.google import GoogleModel + + return GoogleModel(config.model) + except ImportError: + raise ImportError( + "Google AI provider requires google-genai. Install with: uv add pydantic-ai[google]" + ) + + if config.provider == "ollama": + from pydantic_ai.providers.ollama import OllamaProvider + + return OpenAIChatModel( + config.model, + provider=OllamaProvider(base_url=config.base_url), + ) + + raise ValueError(f"Unsupported AI provider: {config.provider}") diff --git a/src/flet_pkg/core/ai/refiner.py b/src/flet_pkg/core/ai/refiner.py new file mode 100644 index 0000000..aabcb88 --- /dev/null +++ b/src/flet_pkg/core/ai/refiner.py @@ -0,0 +1,354 @@ +"""AI refinement orchestrator. + +Coordinates the four-step pipeline: +1. Gap Report (deterministic) +2. Architect (LLM — reasons about improvements) +3. Editor (LLM — produces search/replace edits) +4. Validator (deterministic — syntax check + retry loop) +""" + +import asyncio +import difflib +from pathlib import Path +from typing import Any + +from rich.live import Live +from rich.spinner import Spinner +from rich.table import Table as RichGrid +from rich.text import Text + +from flet_pkg.core.ai.agent import ( + ArchitectDeps, + EditorDeps, + apply_edits, + build_architect_agent, + build_editor_agent, + validate_python_syntax, +) +from flet_pkg.core.ai.config import AIConfig +from flet_pkg.core.ai.gap_analyzer import GapAnalyzer +from flet_pkg.core.ai.models import RefinementResult +from flet_pkg.core.ai.provider import create_model +from flet_pkg.core.models import DartPackageAPI, GenerationPlan + +MAX_RETRIES = 2 + + +class AIRefiner: + """Orchestrates AI-powered code refinement. + + Uses the Architect/Editor pattern: + - Architect reasons about WHAT to fix (structured plan) + - Editor produces HOW to fix it (search/replace edits) + - Validator checks syntax and retries on failure + """ + + def __init__(self, config: AIConfig): + """Initialise the refiner with an AI configuration. + + Args: + config: Provider and model settings for the LLM. + """ + self.config = config + self._model: Any = None + + def _get_model(self) -> Any: + """Lazily create and return the pydantic-ai model instance.""" + if self._model is None: + self._model = create_model(self.config) + return self._model + + def refine( + self, + api: DartPackageAPI, + plan: GenerationPlan, + generated_files: dict[str, str], + extension_type: str, + package_path: Path | None = None, + verbose: bool = False, + ) -> RefinementResult: + """Run the full AI refinement pipeline. + + Args: + api: Parsed Dart package API. + plan: Generation plan from the analyzer. + generated_files: Dict of filename → content from generators. + extension_type: "service" or "ui_control". + package_path: Path to the downloaded Flutter package source. + verbose: If True, print detailed progress at each step. + + Returns: + RefinementResult with gap report, edits applied, and validation status. + """ + return asyncio.run( + self._refine_async(api, plan, generated_files, extension_type, package_path, verbose) + ) + + async def _refine_async( + self, + api: DartPackageAPI, + plan: GenerationPlan, + generated_files: dict[str, str], + extension_type: str, + package_path: Path | None = None, + verbose: bool = False, + ) -> RefinementResult: + """Async implementation of the refinement pipeline.""" + from flet_pkg.ui.console import console + + # Step 1: Gap Report (deterministic — no LLM) + gap_analyzer = GapAnalyzer() + gap_report = gap_analyzer.analyze(api, plan, extension_type) + + if verbose: + console.print( + f" [cyan]├─ Gap Report:[/cyan] {len(gap_report.gaps)} gaps, " + f"{gap_report.feasible_gaps} feasible, " + f"{gap_report.coverage_pct:.1f}%" + ) + for gap in gap_report.gaps: + if gap.feasible: + console.print(f" [dim]│ {gap.kind.value}: {gap.dart_name}[/dim]") + else: + console.print( + f" [dim]│ {gap.kind.value}: {gap.dart_name}" + f" [yellow]\\[infeasible][/yellow][/dim]" + ) + + if gap_report.feasible_gaps == 0: + return RefinementResult( + gap_report=gap_report, + overall_assessment="No feasible gaps found — code is already well-covered.", + validation_passed=True, + ) + + model = self._get_model() + + # Collect Dart source snippets for context + dart_sources: dict[str, str] = {} + if package_path and package_path.exists(): + for dart_file in package_path.rglob("*.dart"): + try: + relative = str(dart_file.relative_to(package_path)) + dart_sources[relative] = dart_file.read_text(encoding="utf-8") + except Exception: + pass + + # Step 2: Architect (LLM — reason about gaps) + architect_agent = build_architect_agent(model) + architect_deps = ArchitectDeps( + gap_report=gap_report, + dart_sources=dart_sources, + generated_files=generated_files, + ) + + architect_prompt = ( + f"Analyze the gap report for the '{plan.flutter_package}' Flutter package " + f"({extension_type} extension). There are {gap_report.feasible_gaps} feasible gaps " + f"out of {len(gap_report.gaps)} total. " + f"Use the tools to inspect the gaps and generated files, then produce " + f"an improvement plan." + ) + + if verbose: + spinner = _tree_spinner("Architect analyzing gaps...") + with Live(spinner, console=console, refresh_per_second=10, transient=True): + architect_result = await architect_agent.run(architect_prompt, deps=architect_deps) + else: + architect_result = await architect_agent.run(architect_prompt, deps=architect_deps) + architect_plan = architect_result.output + + # Track token usage + total_input_tokens = 0 + total_output_tokens = 0 + arch_usage = architect_result.usage() + total_input_tokens += arch_usage.request_tokens or 0 + total_output_tokens += arch_usage.response_tokens or 0 + + if verbose: + n = len(architect_plan.suggestions) + console.print(f" [cyan]├─ Architect:[/cyan] {n} suggestion(s)") + for s in architect_plan.suggestions: + console.print( + f" [dim]│ [/dim][bold]\\[P{s.priority}][/bold] " + f"[dim]{s.target_file}: {s.description}[/dim]" + ) + + if not architect_plan.suggestions: + return RefinementResult( + gap_report=gap_report, + architect_plan=architect_plan, + overall_assessment="Architect found no actionable improvements.", + validation_passed=True, + ) + + # Step 3 + 4: Editor (LLM) + Validator (retry loop) + editor_agent = build_editor_agent(model) + current_files = dict(generated_files) + total_applied = 0 + total_failed = 0 + validation_passed = False + all_diffs: list[tuple[str, str]] = [] + + for attempt in range(1 + MAX_RETRIES): + editor_deps = EditorDeps( + architect_plan=architect_plan, + file_contents=current_files, + feedback="" if attempt == 0 else "Previous attempt had errors. Fix and retry.", + ) + + suggestions_text = "\n".join( + f"- [{s.priority}] {s.target_file}: {s.description}" + for s in architect_plan.suggestions + ) + + editor_prompt = ( + f"Apply these improvements to the generated files:\n\n{suggestions_text}\n\n" + f"Use the tools to read file contents, then produce search/replace edits." + ) + if editor_deps.feedback: + editor_prompt += f"\n\nFeedback from previous attempt: {editor_deps.feedback}" + + if verbose: + with Live( + _tree_spinner(f"Editor applying improvements (attempt {attempt + 1})..."), + console=console, + refresh_per_second=10, + transient=True, + ): + editor_result = await editor_agent.run(editor_prompt, deps=editor_deps) + else: + editor_result = await editor_agent.run(editor_prompt, deps=editor_deps) + + # Track editor token usage + ed_usage = editor_result.usage() + total_input_tokens += ed_usage.request_tokens or 0 + total_output_tokens += ed_usage.response_tokens or 0 + + # Snapshot before applying edits + before_files = dict(current_files) + + # Apply edits + modified_files, applied, failed = apply_edits(editor_result.output.edits, current_files) + total_applied += applied + total_failed += failed + + # Generate unified diffs for modified files + attempt_diffs = _compute_diffs(before_files, modified_files) + all_diffs.extend(attempt_diffs) + + if verbose: + console.print( + f" [cyan]├─ Editor[/cyan] (attempt {attempt + 1}): " + f"{applied} applied, {failed} failed" + ) + # Show colored diffs + for filename, diff_text in attempt_diffs: + for line in diff_text.splitlines(): + colored = _colorize_diff_line(line) + console.print(f" [dim]│[/dim] {colored}") + + # Validate + errors = validate_python_syntax(modified_files) + if not errors: + current_files = modified_files + validation_passed = True + if verbose: + console.print(" [cyan]├─ Validation:[/cyan] [green]passed ✓[/green]") + break + + if verbose: + console.print( + f" [cyan]├─ Validation:[/cyan] [red]FAILED ✗[/red] ({len(errors)} error(s))" + ) + for err in errors[:5]: + console.print(f" [dim]│ {err}[/dim]") + + # Feed errors back for retry + editor_deps.feedback = "Validation errors:\n" + "\n".join(errors) + + # Detect pending suggestions (not applied by Editor) + files_with_diffs = {fname for fname, _ in all_diffs} + pending = [s for s in architect_plan.suggestions if s.target_file not in files_with_diffs] + + if verbose and pending: + console.print(" [cyan]├─ Pending suggestions (not applied):[/cyan]") + for s in pending: + console.print( + f" [dim]│[/dim] [bold]\\[P{s.priority}][/bold] " + f"[dim]{s.target_file}: {s.description}[/dim]" + ) + + # Update generated_files in-place with refined content + if validation_passed: + generated_files.update(current_files) + + if verbose: + + def _fmt_tokens(n: int) -> str: + return f"{n / 1000:.1f}K" if n >= 1000 else str(n) + + total = total_input_tokens + total_output_tokens + console.print( + f" [cyan]└─ Tokens:[/cyan] input {_fmt_tokens(total_input_tokens)}, " + f"output {_fmt_tokens(total_output_tokens)} " + f"[dim](total {_fmt_tokens(total)})[/dim]" + ) + + return RefinementResult( + gap_report=gap_report, + architect_plan=architect_plan, + edits_applied=total_applied, + edits_failed=total_failed, + validation_passed=validation_passed, + overall_assessment=( + f"Applied {total_applied} edits ({total_failed} failed). " + f"Validation: {'passed' if validation_passed else 'FAILED'}." + ), + input_tokens=total_input_tokens, + output_tokens=total_output_tokens, + file_diffs=all_diffs, + pending_suggestions=pending, + ) + + +def _compute_diffs(before: dict[str, str], after: dict[str, str]) -> list[tuple[str, str]]: + """Compute unified diffs for files that changed.""" + diffs: list[tuple[str, str]] = [] + for filename in after: + old = before.get(filename, "") + new = after[filename] + if old == new: + continue + diff_lines = list( + difflib.unified_diff( + old.splitlines(keepends=True), + new.splitlines(keepends=True), + fromfile=f"a/{filename}", + tofile=f"b/{filename}", + ) + ) + if diff_lines: + diffs.append((filename, "".join(diff_lines))) + return diffs + + +def _tree_spinner(label: str) -> RichGrid: + """Create a Rich renderable with the spinner aligned to the tree prefix.""" + spinner = Spinner("dots") + t = RichGrid.grid(padding=0) + t.add_row(Text(" ├─ ", style="cyan"), spinner, Text(f" {label}", style="cyan")) + return t + + +def _colorize_diff_line(line: str) -> str: + """Apply Rich markup to a single diff line.""" + if line.startswith("+++") or line.startswith("---"): + return f"[bold]{line}[/bold]" + if line.startswith("@@"): + return f"[cyan]{line}[/cyan]" + if line.startswith("+"): + return f"[green]{line}[/green]" + if line.startswith("-"): + return f"[red]{line}[/red]" + return line diff --git a/src/flet_pkg/core/analyzer.py b/src/flet_pkg/core/analyzer.py new file mode 100644 index 0000000..f0f0083 --- /dev/null +++ b/src/flet_pkg/core/analyzer.py @@ -0,0 +1,2438 @@ +""" +Package analyzer for Dart-to-Python code generation. + +Analyzes a ``DartPackageAPI`` and produces a ``GenerationPlan`` that +drives the code generators: detects namespaces, events, properties, +and maps types. +""" + +import keyword +import re +from statistics import median + +from flet_pkg.core.models import ( + DartClass, + DartMethod, + DartPackageAPI, + DartParam, + EnumPlan, + EventPlan, + GenerationPlan, + MethodPlan, + ParamPlan, + PropertyPlan, + SiblingWidgetPlan, + StubDataClass, + SubControlPlan, + SubModulePlan, + WidgetVariant, +) +from flet_pkg.core.parser import camel_to_snake, parse_dart_package_api +from flet_pkg.core.type_map import ( + get_flet_dart_getter, + map_dart_type, + map_dart_type_flet, + map_return_type, +) + +# Internal Dart methods that should never appear in the generated Python API. +# These are framework/language internals, not user-facing functionality. +_INTERNAL_METHODS = frozenset( + { + "hashCode", + "getHashCode", + "runtimeType", + "toString", + "noSuchMethod", + "toJson", + "fromJson", + "toMap", + "fromMap", + "copyWith", + "compareTo", + "toJsonString", + "fromJsonFunction", + "getFromJsonFunction", + "removeDuplicates", + "addIfNotNull", + "compute", + "identical", + "jsonRepresentation", + "convertToJsonString", + "jsonEncode", + # Dart control-flow keywords that the parser may capture as methods + "catch", + "catch_", + "finally", + "finally_", + "then", + "then_", + "rethrow", + "rethrow_", + } +) + + +def _safe_module_name(name: str) -> str: + """Ensure a module/namespace name is not a Python keyword. + + Python keywords like ``async``, ``await``, ``class`` cannot be used as + module filenames or property accessors. Appends ``_ops`` suffix when + a collision is detected (e.g. ``async`` → ``async_ops``). + """ + if keyword.iskeyword(name): + return f"{name}_ops" + return name + + +class PackageAnalyzer: + """Analyzes a parsed Dart API and produces a generation plan. + + The analyzer detects: + - **Namespaces**: Groups of related methods (e.g., User, Notifications) + that become separate Python sub-module files. + - **Events**: Listener/Observer methods that become ``ft.EventHandler`` attrs. + - **Properties**: Constructor params and getters that become dataclass fields. + - **Enums**: Dart enums that become Python ``Enum`` classes. + + Args: + min_namespace_methods: Minimum methods to justify a separate sub-module. + """ + + def __init__(self, min_namespace_methods: int = 2): + """Initialise the analyzer. + + Args: + min_namespace_methods: Minimum number of methods required to + justify extracting a separate sub-module. + """ + self.min_namespace_methods = min_namespace_methods + self._known_types: frozenset[str] = frozenset() + + def analyze( + self, + api: DartPackageAPI, + control_name: str, + extension_type: str, + flutter_package: str = "", + package_name: str = "", + description: str = "", + ) -> GenerationPlan: + """Analyze a Dart API and produce a GenerationPlan.""" + base_class = "ft.Service" if extension_type == "service" else "ft.LayoutControl" + control_name_lower = control_name.lower() + + # Build set of known type names for type sanitization. + # Includes re-exported types AND locally parsed enum names and + # helper class names so they are not replaced with `dict | None`. + known = set(api.reexported_types.keys()) + known.update(e.name for e in api.enums) + known.update(h.name for h in api.helper_classes) + self._known_types = frozenset(known) + + # Detect the main SDK class (the one matching the control name) + dart_main_class = self._detect_main_class(api, control_name) + + plan = GenerationPlan( + control_name=control_name, + package_name=package_name, + base_class=base_class, + description=description, + flutter_package=flutter_package, + dart_import=f"package:{flutter_package}/{flutter_package}.dart", + dart_main_class=dart_main_class, + ) + + # --- UI control path: widget constructor params → properties --- + has_widget_classes = any(cls.constructor_params for cls in api.classes) + if extension_type == "ui_control" and has_widget_classes: + self._process_widget_classes(api, plan, control_name) + else: + # --- Service path: methods → async methods --- + # Detect namespaces by class name prefix + namespace_classes: dict[str, list[DartClass]] = {} + main_classes: list[DartClass] = [] + + # Single-class packages: the only class IS the main class, no sub-modules + if len(api.classes) == 1: + main_classes = api.classes[:] + else: + for cls in api.classes: + ns = self._detect_namespace(cls, control_name, control_name_lower) + if ns: + namespace_classes.setdefault(ns, []).append(cls) + else: + main_classes.append(cls) + + # Infer properties from initialize() method on main class + for cls in main_classes: + self._infer_properties_from_initialize(cls, plan) + + # Infer properties from pre-init setter methods (consent, log level, etc.) + self._infer_pre_init_properties(api, plan, control_name) + + # Process main classes: extract events, properties, methods + for cls in main_classes: + self._process_main_class(cls, plan, dart_main_class, api) + + # Detect sub-object namespaces (e.g., User.pushSubscription → fold into user) + self._fold_sub_objects(api, namespace_classes, control_name, plan) + + # Process namespace classes into sub-modules + for ns_name, classes in namespace_classes.items(): + # Extract events from namespace classes (events go to main plan) + for cls in classes: + for method in cls.methods: + event = self._detect_event( + method, control_name, api, dart_main_class, cls.name + ) + if event and not any( + e.python_attr_name == event.python_attr_name for e in plan.events + ): + plan.events.append(event) + + sub_module = self._build_sub_module( + ns_name, classes, control_name, dart_main_class, plan + ) + if sub_module and len(sub_module.methods) >= self.min_namespace_methods: + plan.sub_modules.append(sub_module) + else: + # Not enough methods for a sub-module; merge into main + for cls in classes: + self._process_main_class(cls, plan, dart_main_class, api) + + # Process top-level functions as main methods + for func in api.top_level_functions: + method_plan = self._build_method_plan(func, "", dart_main_class) + if not any(m.python_name == method_plan.python_name for m in plan.main_methods): + plan.main_methods.append(method_plan) + + # Process enums — only keep those that are used or commonly useful + # Start with any enums already added (e.g. family type enum) + generated_type_names: set[str] = {e.python_name for e in plan.enums} + for dart_enum in api.enums: + plan.enums.append( + EnumPlan( + python_name=dart_enum.name, + values=[(v, v.lower(), doc) for v, doc in dart_enum.values], + docstring=dart_enum.docstring, + ) + ) + generated_type_names.add(dart_enum.name) + + # Generate stub types for re-exported types from platform_interface + # that are referenced by methods but not locally defined. + self._generate_reexported_types(api, plan, generated_type_names) + + # Generate data classes from local helper classes (Configuration, + # Options, Params, etc.) that are referenced as method parameters. + self._generate_local_data_classes(api, plan, generated_type_names) + + # Post-process: re-sanitize types against actually-generated names. + # Types from known_types that were NOT generated must be replaced + # with dict | None (e.g. WorkoutRouteLocation → dict | None). + # Include sub-control names so they don't get sanitized away. + for sc in plan.sub_controls: + generated_type_names.add(sc.control_name) + _post_sanitize_property_types(plan, generated_type_names) + + # Set error event class name with short prefix + prefix = "OS" if control_name.lower().startswith("one") else control_name + plan.error_event_class = f"{prefix}ErrorEvent" + + # Build dart_listeners from events + plan.dart_listeners = [ + { + "event_name": event.dart_event_name, + "python_attr": event.python_attr_name, + "dart_listener_method": event.dart_listener_method, + "dart_sdk_accessor": event.dart_sdk_accessor, + } + for event in plan.events + ] + + return plan + + def resolve_platform_types( + self, + api: DartPackageAPI, + plan: GenerationPlan, + ) -> None: + """Download platform_interface packages and resolve stub types. + + Replaces UNKNOWN-only enum stubs and ``data: dict`` stub data + classes with real values/fields parsed from the source packages. + """ + from flet_pkg.core.downloader import PubDevDownloader + + # Collect unique source packages that need resolution + packages_to_resolve: dict[str, list[str]] = {} + for type_name, source_pkg in api.reexported_types.items(): + if not source_pkg: + continue + packages_to_resolve.setdefault(source_pkg, []).append(type_name) + + if not packages_to_resolve: + return + + downloader = PubDevDownloader() + + for pkg_name, type_names in packages_to_resolve.items(): + try: + pkg_path = downloader.download(pkg_name) + except Exception: + continue + + try: + platform_api = parse_dart_package_api(pkg_path) + except Exception: + continue + + # Build lookup maps from the platform_interface source + enum_map: dict[str, list[tuple[str, str]]] = {} + for dart_enum in platform_api.enums: + enum_map[dart_enum.name] = dart_enum.values + + helper_map: dict[str, list[tuple[str, str]]] = {} + for helper_cls in platform_api.helper_classes: + fields = [] + for method in helper_cls.methods: + if method.is_getter and not method.params: + if method.name == helper_cls.name: + continue + field_name = camel_to_snake(method.name) + field_type = map_dart_type(method.return_type) + fields.append((field_name, field_type)) + if fields: + helper_map[helper_cls.name] = fields + + # Standard Dart object members to skip when extracting fields + _OBJECT_MEMBERS = { + "hashCode", + "runtimeType", + "toString", + "noSuchMethod", + "operator", + "hash_code", + "runtime_type", + } + + def _clean_fields(raw_fields: list[tuple[str, str]]) -> list[tuple[str, str]]: + """Filter out Dart object members and sanitize types.""" + cleaned = [] + for fname, ftype in raw_fields: + if fname in _OBJECT_MEMBERS: + continue + # Sanitize unknown types to str | None + ftype = _sanitize_python_type(ftype, self._known_types) + cleaned.append((fname, ftype)) + return cleaned + + # Also check regular classes for data classes with getters/fields + for cls in platform_api.classes: + if cls.name in helper_map: + continue + fields = [] + for method in cls.methods: + if method.is_getter and not method.params: + if method.name == cls.name: + continue + field_name = camel_to_snake(method.name) + field_type = map_dart_type(method.return_type) + fields.append((field_name, field_type)) + if fields: + helper_map[cls.name] = fields + + # Clean all fields + for name in helper_map: + helper_map[name] = _clean_fields(helper_map[name]) + + # Update plan enums with real values + for enum_plan in plan.enums: + if enum_plan.python_name in enum_map: + real_values = enum_map[enum_plan.python_name] + if real_values: + enum_plan.values = [(v, v.lower(), doc) for v, doc in real_values] + enum_plan.docstring = "" + + # Convert stub data classes to enums when platform_interface + # reveals they are actually enums (not data classes). + stubs_to_remove: list[StubDataClass] = [] + for stub in plan.stub_data_classes: + if stub.python_name in enum_map: + real_values = enum_map[stub.python_name] + if real_values and not any( + e.python_name == stub.python_name for e in plan.enums + ): + plan.enums.append( + EnumPlan( + python_name=stub.python_name, + values=[(v, v.lower(), doc) for v, doc in real_values], + ) + ) + stubs_to_remove.append(stub) + elif stub.python_name in helper_map: + real_fields = helper_map[stub.python_name] + if real_fields: + stub.fields = real_fields + stub.docstring = "" + for stub in stubs_to_remove: + plan.stub_data_classes.remove(stub) + + # ------------------------------------------------------------------ + # Widget class processing (ui_control path) + # ------------------------------------------------------------------ + + # Dart callback/function types that become event handlers + _CALLBACK_TYPES = { + "VoidCallback", + "GestureTapCallback", + "GestureLongPressCallback", + } + _CALLBACK_PREFIXES = ( + "ValueChanged", + "ValueSetter", + "ValueGetter", + "IndexedWidgetBuilder", + "WidgetBuilder", + "NullableIndexedWidgetBuilder", + ) + + # Dart types that cannot be serialized to Flet properties (complex objects). + # Note: simple Flutter enums (Curve, StackFit, Clip, WrapAlignment, etc.) + # are handled by the type_map and should NOT be listed here. + _NON_SERIALIZABLE_TYPES = { + "LinearGradient", + "Gradient", + "MaskFilter", + "Curves", + "ScrollPhysics", + "NeverScrollableScrollPhysics", + "AlwaysScrollableScrollPhysics", + "EdgeInsetsGeometry", + "EdgeInsets", + "BorderRadius", + "BorderSide", + "BoxDecoration", + "ShapeBorder", + "BoxBorder", + # Note: TextStyle is handled by _FLET_TYPE_MAP -> ft.TextStyle + # Controller types (complex objects, not properties) + "AnimationController", + "ScrollController", + "TextEditingController", + # Cursor / hit-test types + "MouseCursor", + "SystemMouseCursors", + "HitTestBehavior", + } + + def _process_widget_classes( + self, + api: DartPackageAPI, + plan: GenerationPlan, + control_name: str, + ) -> None: + """Process widget classes: extract constructor params as properties. + + For ``ui_control`` extensions, widget constructor parameters become + Flet control properties. Callback params become event handlers. + + Dispatches to one of three strategies: + - **family**: Many widgets with shared params → single control + type enum + - **sibling**: 2-4 distinct widgets → separate controls + - **single**: One widget → original single-widget path + """ + widget_classes = [c for c in api.classes if c.constructor_params] + if not widget_classes: + return + + strategy, filtered = self._classify_multi_widgets(widget_classes, control_name) + + if strategy == "family": + self._process_widget_family(api, plan, filtered, control_name) + return + if strategy == "sibling": + self._process_sibling_widgets(api, plan, filtered, control_name) + return + + # "single" → original single-widget code path + self._process_single_widget(api, plan, widget_classes, control_name) + + def _classify_multi_widgets( + self, + widget_classes: list[DartClass], + control_name: str, + ) -> tuple[str, list[DartClass]]: + """Classify widget classes into family, sibling, or single strategy. + + Returns ``(strategy, filtered_classes)`` where strategy is one of + ``"family"``, ``"sibling"``, or ``"single"``. + """ + # Filter out private/internal widgets (reuse _select_main_widget logic) + filtered = [ + cls + for cls in widget_classes + if not cls.name.startswith("_") + and not any(cls.name.endswith(s) for s in self._INTERNAL_WIDGET_SUFFIXES) + ] + if not filtered: + filtered = widget_classes + + if len(filtered) <= 1: + return ("single", filtered) + + # Compute param name sets per widget (excluding framework params) + _FRAMEWORK_PARAMS = {"key", "child", "children"} + param_sets: list[set[str]] = [] + for cls in filtered: + names = {p.name for p in cls.constructor_params if p.name not in _FRAMEWORK_PARAMS} + param_sets.append(names) + + # Shared params = intersection of ALL widget param sets + shared = param_sets[0].copy() + for s in param_sets[1:]: + shared &= s + + # Median param count across widgets + counts = [len(s) for s in param_sets] + med = median(counts) if counts else 1 + + shared_ratio = len(shared) / med if med > 0 else 0.0 + + # Family: ≥5 widgets, OR 2-4 with high overlap (≥0.6) + if len(filtered) >= 5 or (2 <= len(filtered) <= 4 and shared_ratio >= 0.6): + return ("family", filtered) + + # Sibling: 2-4 widgets with low overlap + if 2 <= len(filtered) <= 4 and shared_ratio < 0.6: + return ("sibling", filtered) + + return ("single", filtered) + + def _process_single_widget( + self, + api: DartPackageAPI, + plan: GenerationPlan, + widget_classes: list[DartClass], + control_name: str, + ) -> None: + """Original single-widget processing path.""" + main_widget = self._select_main_widget(widget_classes, control_name) + plan.dart_main_class = main_widget.name + + helper_map: dict[str, DartClass] = {h.name: h for h in api.helper_classes} + referenced_helpers: set[str] = set() + + sub_controls = self._detect_sub_controls( + main_widget, widget_classes, api.component_classes, helper_map, plan + ) + plan.sub_controls = sub_controls + sub_control_names = {sc.control_name for sc in self._flatten_sub_controls(sub_controls)} + + for param in main_widget.constructor_params: + result = self._process_widget_param( + param, + plan, + control_name, + helper_map, + referenced_helpers, + is_ui=True, + sub_control_names=sub_control_names, + ) + if result is not None: + if isinstance(result, EventPlan): + if not any(e.python_attr_name == result.python_attr_name for e in plan.events): + plan.events.append(result) + else: + if not any(p.python_name == result.python_name for p in plan.properties): + plan.properties.append(result) + + self._generate_widget_helpers(api, plan, helper_map, referenced_helpers, sub_control_names) + + def _process_widget_family( + self, + api: DartPackageAPI, + plan: GenerationPlan, + widget_classes: list[DartClass], + control_name: str, + ) -> None: + """Process a family of widgets with shared params into a single control + type enum.""" + _FRAMEWORK_PARAMS = {"key", "child", "children"} + + # Compute shared param names across all family members + param_sets = [ + {p.name for p in cls.constructor_params if p.name not in _FRAMEWORK_PARAMS} + for cls in widget_classes + ] + shared_names = param_sets[0].copy() + for s in param_sets[1:]: + shared_names &= s + + # Use widget with most params as "template" for types + template = max(widget_classes, key=lambda c: len(c.constructor_params)) + plan.dart_main_class = template.name + + helper_map: dict[str, DartClass] = {h.name: h for h in api.helper_classes} + referenced_helpers: set[str] = set() + + # Process shared params via existing _process_widget_param + for param in template.constructor_params: + if param.name not in shared_names: + continue + result = self._process_widget_param( + param, + plan, + control_name, + helper_map, + referenced_helpers, + is_ui=True, + sub_control_names=set(), + ) + if result is not None: + if isinstance(result, EventPlan): + if not any(e.python_attr_name == result.python_attr_name for e in plan.events): + plan.events.append(result) + else: + if not any(p.python_name == result.python_name for p in plan.properties): + plan.properties.append(result) + + # Create WidgetVariant per class + control_lower = control_name.lower() + variants: list[WidgetVariant] = [] + for cls in widget_classes: + # Strip control_name prefix to derive the variant value + raw_suffix = cls.name + if cls.name.lower().startswith(control_lower): + raw_suffix = cls.name[len(control_name) :] + enum_val = camel_to_snake(raw_suffix) if raw_suffix else cls.name.lower() + # Strip leading underscore from enum value + enum_val = enum_val.lstrip("_") + if not enum_val: + enum_val = cls.name.lower() + variants.append(WidgetVariant(dart_class_name=cls.name, enum_value=enum_val)) + + plan.widget_family_variants = variants + + # Create the type enum + enum_name = f"{control_name}Type" + enum_values = [ + (v.enum_value.upper(), v.enum_value, f"{v.dart_class_name} variant.") for v in variants + ] + family_enum = EnumPlan( + python_name=enum_name, + values=enum_values, + docstring=f"Widget variant type for {control_name}.", + ) + plan.widget_family_enum = family_enum + plan.enums.insert(0, family_enum) + + # Insert "type" property at the beginning of plan.properties + plan.properties.insert( + 0, + PropertyPlan( + python_name="type", + python_type=f"{enum_name} | None", + default_value="None", + docstring=f"Widget variant type. One of {enum_name} values.", + dart_name="type", + dart_getter='widget.control.getString("type")', + ), + ) + + self._generate_widget_helpers(api, plan, helper_map, referenced_helpers, set()) + + def _process_sibling_widgets( + self, + api: DartPackageAPI, + plan: GenerationPlan, + widget_classes: list[DartClass], + control_name: str, + ) -> None: + """Process sibling widgets: one main + N siblings as separate controls.""" + main_widget = self._select_main_widget(widget_classes, control_name) + plan.dart_main_class = main_widget.name + + helper_map: dict[str, DartClass] = {h.name: h for h in api.helper_classes} + referenced_helpers: set[str] = set() + + # Process main widget normally + sub_controls = self._detect_sub_controls( + main_widget, widget_classes, api.component_classes, helper_map, plan + ) + plan.sub_controls = sub_controls + sub_control_names = {sc.control_name for sc in self._flatten_sub_controls(sub_controls)} + + for param in main_widget.constructor_params: + result = self._process_widget_param( + param, + plan, + control_name, + helper_map, + referenced_helpers, + is_ui=True, + sub_control_names=sub_control_names, + ) + if result is not None: + if isinstance(result, EventPlan): + if not any(e.python_attr_name == result.python_attr_name for e in plan.events): + plan.events.append(result) + else: + if not any(p.python_name == result.python_name for p in plan.properties): + plan.properties.append(result) + + # Process each remaining widget as a sibling + for cls in widget_classes: + if cls.name == main_widget.name: + continue + + sibling = SiblingWidgetPlan( + control_name=cls.name, + dart_class_name=cls.name, + control_name_snake=camel_to_snake(cls.name), + ) + + sib_helpers: set[str] = set() + for param in cls.constructor_params: + result = self._process_widget_param( + param, + plan, + cls.name, + helper_map, + sib_helpers, + is_ui=True, + sub_control_names=set(), + ) + if result is not None: + if isinstance(result, EventPlan): + sibling.events.append(result) + else: + sibling.properties.append(result) + referenced_helpers.update(sib_helpers) + + plan.sibling_widgets.append(sibling) + + self._generate_widget_helpers(api, plan, helper_map, referenced_helpers, sub_control_names) + + def _generate_widget_helpers( + self, + api: DartPackageAPI, + plan: GenerationPlan, + helper_map: dict[str, DartClass], + referenced_helpers: set[str], + sub_control_names: set[str], + ) -> None: + """Generate dataclasses for referenced helper types (shared by all widget strategies).""" + generated_names = {e.python_name for e in plan.enums} + generated_names.update(s.python_name for s in plan.stub_data_classes) + generated_names.update(sub_control_names) + pending = list(referenced_helpers) + while pending: + helper_name = pending.pop(0) + if helper_name in generated_names: + continue + if helper_name not in helper_map: + continue + helper_cls = helper_map[helper_name] + fields = [] + for method in helper_cls.methods: + if method.is_getter and not method.params: + if method.name == helper_cls.name: + continue + field_name = camel_to_snake(method.name) + field_type = map_dart_type(method.return_type) + field_type = _sanitize_python_type(field_type, self._known_types) + fields.append((field_name, field_type)) + raw_base = re.sub(r"[?\s|].*", "", method.return_type).strip() + if raw_base in helper_map and raw_base not in generated_names: + pending.append(raw_base) + plan.stub_data_classes.append( + StubDataClass( + python_name=helper_name, + fields=fields, + docstring=helper_cls.docstring, + ) + ) + generated_names.add(helper_name) + + _post_sanitize_property_types(plan, generated_names) + + def _process_widget_param( + self, + param: DartParam, + plan: GenerationPlan, + control_name: str, + helper_map: dict[str, DartClass], + referenced_helpers: set[str], + is_ui: bool = True, + sub_control_names: set[str] | None = None, + ) -> PropertyPlan | EventPlan | None: + """Process a single widget constructor param into a property or event. + + Returns a ``PropertyPlan``, ``EventPlan``, or ``None`` (skip). + """ + from flet_pkg.core.generators.base import CodeGenerator + + base_type = re.sub(r"[?<].*", "", param.dart_type).strip() + + # Detect callbacks → event handlers + is_callback = ( + base_type in self._CALLBACK_TYPES + or base_type.startswith(self._CALLBACK_PREFIXES) + or "Function" in param.dart_type + or "Callback" in param.dart_type + ) + if not is_callback and re.match(r"on[A-Z]", param.name): + is_callback = True + + if is_callback: + python_attr = camel_to_snake(param.name) + if not python_attr.startswith("on_"): + python_attr = f"on_{python_attr}" + event_class = f"{control_name}{param.name[0].upper()}{param.name[1:]}Event" + return EventPlan( + python_attr_name=python_attr, + event_class_name=event_class, + dart_event_name=python_attr.removeprefix("on_"), + dart_listener_method=param.name, + ) + + # Skip non-serializable Flutter types + if base_type in self._NON_SERIALIZABLE_TYPES: + return None + if "Controller" in base_type or base_type.endswith("Painter"): + return None + + # Check if this type references a package helper class + if base_type in helper_map: + referenced_helpers.add(base_type) + + # Check for generic type params (e.g., List) + generic_match = re.search(r"<(\w+)>", param.dart_type) + if generic_match: + inner_type = generic_match.group(1) + if inner_type in helper_map: + referenced_helpers.add(inner_type) + + # Check if this param is a sub-control type + if sub_control_names is None: + sub_control_names = set() + + # Resolve the type: if this is a sub-control, use its class name + is_sub_control_type = base_type in sub_control_names + is_list_sub_control = False + if generic_match and generic_match.group(1) in sub_control_names: + is_sub_control_type = True + is_list_sub_control = True + + python_name = camel_to_snake(param.name) + + if is_sub_control_type: + # generic_match is guaranteed non-None when is_list_sub_control=True + sc_name = ( + generic_match.group(1) if (is_list_sub_control and generic_match) else base_type + ) + if is_list_sub_control: + python_type = f"list[{sc_name}]" + default_value = "field(default_factory=list)" + dart_getter = f'buildWidgets("{python_name}")' + else: + is_nullable = param.dart_type.endswith("?") + python_type = f"{sc_name} | None" if is_nullable else sc_name + default_value = "None" + if "None" not in python_type: + python_type = f"{python_type} | None" + dart_getter = f'buildWidget("{python_name}")' + return PropertyPlan( + python_name=python_name, + python_type=python_type, + default_value=default_value, + docstring="", + dart_name=param.name, + dart_getter=dart_getter, + ) + + # Convert to Python property (Flet-aware for ui_control) + if is_ui: + python_type = map_dart_type_flet(param.dart_type) + if python_type is None: + return None + else: + python_type = map_dart_type(param.dart_type) + + python_type = _sanitize_python_type(python_type, self._known_types) + + # Determine default value + default_value = CodeGenerator._py_default(param.default) + + # Use field(default_factory=list) for list types (before nullable logic) + if python_type.startswith("list[") and default_value in ("None", '""'): + default_value = "field(default_factory=list)" + + # In Flet controls, all properties are dataclass fields and need + # defaults. If default is None, the type must be nullable. + if default_value == "None" and "None" not in python_type: + python_type = f"{python_type} | None" + + # Compute Dart getter expression for UI controls + dart_getter = "" + if is_ui: + dart_getter = get_flet_dart_getter(python_type, python_name) + + return PropertyPlan( + python_name=python_name, + python_type=python_type, + default_value=default_value, + docstring="", + dart_name=param.name, + dart_getter=dart_getter, + ) + + def _detect_sub_controls( + self, + main_widget: DartClass, + widget_classes: list[DartClass], + component_classes: list[DartClass], + helper_map: dict[str, DartClass], + plan: GenerationPlan, + depth: int = 1, + max_depth: int = 3, + ) -> list[SubControlPlan]: + """Detect compound widget sub-controls from constructor param types. + + Recursively identifies constructor parameters whose types match + other classes in the same package, generating ``SubControlPlan`` + entries for each. + """ + if depth > max_depth: + return [] + + # Build candidate map: all widgets + components, excluding main + candidates: dict[str, DartClass] = {} + for cls in widget_classes: + if cls.name != main_widget.name: + candidates[cls.name] = cls + for cls in component_classes: + candidates[cls.name] = cls + + helper_names = set(helper_map.keys()) + sub_controls: list[SubControlPlan] = [] + + for param in main_widget.constructor_params: + base_type = re.sub(r"[?<].*", "", param.dart_type).strip() + is_list = False + + # Check for List pattern + generic_match = re.search(r"<(\w+)>", param.dart_type) + if generic_match: + inner = generic_match.group(1) + if inner in candidates and param.dart_type.startswith("List"): + base_type = inner + is_list = True + elif inner in candidates: + base_type = inner + + # Skip Widget (already ft.Control), helpers, and non-candidates + if base_type == "Widget" or base_type in helper_names: + continue + if base_type not in candidates: + continue + + candidate = candidates[base_type] + python_name = camel_to_snake(param.name) + + # Process params of the sub-control + sub_props: list[PropertyPlan] = [] + sub_events: list[EventPlan] = [] + dummy_helpers: set[str] = set() + + for sub_param in candidate.constructor_params: + result = self._process_widget_param( + sub_param, + plan, + base_type, + helper_map, + dummy_helpers, + is_ui=True, + sub_control_names=set(), + ) + if result is None: + continue + if isinstance(result, EventPlan): + sub_events.append(result) + else: + sub_props.append(result) + + # Recurse for nested sub-controls + nested = self._detect_sub_controls( + candidate, + widget_classes, + component_classes, + helper_map, + plan, + depth=depth + 1, + max_depth=max_depth, + ) + + sub_controls.append( + SubControlPlan( + control_name=base_type, + dart_class_name=candidate.name, + properties=sub_props, + events=sub_events, + parent_property=python_name, + is_list=is_list, + sub_controls=nested, + depth=depth, + ) + ) + + return sub_controls + + @staticmethod + def _flatten_sub_controls( + sub_controls: list[SubControlPlan], + ) -> list[SubControlPlan]: + """Flatten a recursive sub-control tree into a list (leaves first).""" + result: list[SubControlPlan] = [] + seen: set[str] = set() + for sc in sub_controls: + for nested in PackageAnalyzer._flatten_sub_controls(sc.sub_controls): + if nested.control_name not in seen: + result.append(nested) + seen.add(nested.control_name) + if sc.control_name not in seen: + result.append(sc) + seen.add(sc.control_name) + return result + + # Suffixes that indicate internal/non-user-facing widget classes. + _INTERNAL_WIDGET_SUFFIXES = ( + "SharedTexture", + "Renderer", + "Controller", + "State", + "Painter", + "Delegate", + "Builder", + ) + + def _select_main_widget(self, widget_classes: list[DartClass], control_name: str) -> DartClass: + """Select the best-matching widget class for the control name. + + Filters out private Dart classes (``_Foo``) and internal helper + widgets (e.g. ``SharedTexture``, ``Renderer``, ``Controller``). + """ + control_lower = control_name.lower() + + # Filter out internal/private widget classes + filtered = [ + cls + for cls in widget_classes + if not cls.name.startswith("_") + and not any(cls.name.endswith(suffix) for suffix in self._INTERNAL_WIDGET_SUFFIXES) + ] + # Fallback to unfiltered if everything got excluded + if not filtered: + filtered = widget_classes + + # Priority 1: Exact name match + for cls in filtered: + if cls.name.lower() == control_lower: + return cls + # Priority 2: Name contains control name + for cls in filtered: + if control_lower in cls.name.lower(): + return cls + # Priority 3: Most constructor params (richest API) + return max(filtered, key=lambda c: len(c.constructor_params)) + + # ------------------------------------------------------------------ + # Main SDK class detection + # ------------------------------------------------------------------ + + def _detect_main_class(self, api: DartPackageAPI, control_name: str) -> str: + """Detect the main SDK class from the package.""" + control_lower = control_name.lower() + # Priority 1: Exact match + for cls in api.classes: + if cls.name.lower() == control_lower: + return cls.name + # Priority 2: Match without "Flutter" prefix + for cls in api.classes: + name = cls.name.lower() + if name == control_lower.replace("flutter", ""): + return cls.name + # Priority 3: Flutter{Name}Plugin pattern (e.g. FlutterLocalNotificationsPlugin) + for cls in api.classes: + stripped = cls.name.lower().removeprefix("flutter").removesuffix("plugin") + if stripped == control_lower: + return cls.name + # Priority 4: Shortest class that starts with control name prefix + candidates = [cls for cls in api.classes if cls.name.lower().startswith(control_lower)] + if candidates: + return min(candidates, key=lambda c: len(c.name)).name + # Fallback: control_name as-is + return control_name + + # ------------------------------------------------------------------ + # Namespace detection + # ------------------------------------------------------------------ + + def _detect_namespace( + self, cls: DartClass, control_name: str, control_name_lower: str + ) -> str | None: + """Detect if a class belongs to a namespace (sub-module).""" + cls_lower = cls.name.lower() + + # Skip the main class itself + if cls_lower == control_name_lower: + return None + + # Skip Flutter{Name}Plugin pattern — it IS the main class + stripped = cls_lower.removeprefix("flutter").removesuffix("plugin") + if stripped == control_name_lower: + return None + + # Check prefix match: e.g., OneSignalUser -> "user" + if cls_lower.startswith(control_name_lower): + suffix = cls.name[len(control_name) :] + if suffix: + # Only treat as namespace if suffix starts on a PascalCase + # boundary (uppercase). "LocalAuthentication" with control + # "LocalAuth" → suffix "entication" (lowercase) = mid-word split. + if not suffix[0].isupper(): + return None + ns = camel_to_snake(suffix) + return _safe_module_name(ns) + + # Check if the class file suggests a namespace + if cls.source_file: + stem = cls.source_file.rsplit("/", maxsplit=1)[-1].replace(".dart", "") + if stem != control_name_lower and not stem.startswith("_"): + return _safe_module_name(stem) + + return None + + # ------------------------------------------------------------------ + # SDK accessor computation + # ------------------------------------------------------------------ + + def _compute_sdk_accessor(self, cls_name: str, control_name: str, dart_main_class: str) -> str: + """Compute the SDK accessor path for a class. + + Examples: + OneSignalUser with main=OneSignal -> "OneSignal.User" + OneSignalNotifications -> "OneSignal.Notifications" + """ + if cls_name == dart_main_class: + return dart_main_class + # Strip the control name prefix to get the namespace suffix + control_len = len(control_name) + if cls_name.lower().startswith(control_name.lower()) and len(cls_name) > control_len: + suffix = cls_name[control_len:] + # Only use suffix if it starts on a PascalCase boundary + if suffix[0].isupper(): + return f"{dart_main_class}.{suffix}" + return dart_main_class + + # ------------------------------------------------------------------ + # Property inference from initialize() method + # ------------------------------------------------------------------ + + def _infer_properties_from_initialize(self, cls: DartClass, plan: GenerationPlan) -> None: + """Infer properties from the main class's initialize() method. + + For example, `OneSignal.initialize(String appId)` produces a + property `app_id: str`. + """ + for method in cls.methods: + if method.name != "initialize": + continue + for param in method.params: + python_name = camel_to_snake(param.name) + python_type = map_dart_type(param.dart_type) + if not any(p.python_name == python_name for p in plan.properties): + plan.properties.append( + PropertyPlan( + python_name=python_name, + python_type=python_type, + default_value=_default_for_type(python_type), + docstring=f"The {python_name.replace('_', ' ')} " + f"for {plan.control_name}.", + ) + ) + + # ------------------------------------------------------------------ + # Sub-object folding (e.g., User.pushSubscription → user module) + # ------------------------------------------------------------------ + + def _fold_sub_objects( + self, + api: DartPackageAPI, + namespace_classes: dict[str, list[DartClass]], + control_name: str, + plan: GenerationPlan | None = None, + ) -> None: + """Fold sub-object namespaces into their parent namespace. + + Detects when a namespace class (e.g., ``OneSignalPushSubscription``) + is accessed as a getter on another namespace class (e.g., + ``OneSignalUser.pushSubscription``). When found, the sub-object's + methods are renamed with a short prefix and merged into the parent. + + Events from folded classes are extracted before listener methods + are removed, ensuring they appear on the main control. + """ + # Build class name → namespace mapping + class_to_ns: dict[str, str] = {} + for ns, classes in namespace_classes.items(): + for cls in classes: + class_to_ns[cls.name] = ns + + # Scan for getter methods that return another namespace class + # (child_ns, parent_ns, getter_name_prefix) + folds: list[tuple[str, str, str]] = [] + for cls in api.classes: + parent_ns = class_to_ns.get(cls.name) + if not parent_ns: + continue + for method in cls.methods: + if not method.is_getter or method.params: + continue + ret_clean = method.return_type.rstrip("?") + child_ns = class_to_ns.get(ret_clean) + if child_ns and child_ns != parent_ns: + # Derive a short prefix from the getter name + # e.g., "pushSubscription" → "push" + getter_snake = camel_to_snake(method.name) + short_prefix = getter_snake.split("_")[0] + folds.append((child_ns, parent_ns, short_prefix)) + + # Execute folds: extract events first, then rename and merge + for child_ns, parent_ns, prefix in folds: + if child_ns not in namespace_classes or parent_ns not in namespace_classes: + continue + + # Extract events from child classes BEFORE removing listener methods + if plan: + for cls in namespace_classes[child_ns]: + for method in cls.methods: + event = self._detect_event( + method, + control_name, + api, + plan.dart_main_class, + cls.name, + ) + if event and not any( + e.python_attr_name == event.python_attr_name for e in plan.events + ): + plan.events.append(event) + + # Find the full getter name for richer naming (e.g., "push_subscription") + full_getter_snake = "" + for cls_check in api.classes: + if class_to_ns.get(cls_check.name) != parent_ns: + continue + for m in cls_check.methods: + if m.is_getter and class_to_ns.get(m.return_type.rstrip("?")) == child_ns: + full_getter_snake = camel_to_snake(m.name) + break + + for cls in namespace_classes[child_ns]: + # Filter out listener/observer methods before renaming + cls.methods = [ + m + for m in cls.methods + if not re.match(r"add\w*(Listener|Observer|Handler)$", m.name) + and not re.match(r"remove\w*(Listener|Observer|Handler)$", m.name) + ] + # Rename methods using intelligent prefix strategy + for method in cls.methods: + if method.name.lower().startswith(prefix): + continue # Already has prefix + if method.is_getter and not method.params: + # Getters use full name: id → GetPushSubscriptionId + cap_full = ( + "".join(w.capitalize() for w in full_getter_snake.split("_")) + if full_getter_snake + else prefix.capitalize() + ) + method.name = "Get" + cap_full + method.name[0].upper() + method.name[1:] + elif method.return_type in ("void", "Future") and not method.params: + # Void no-param actions: optIn → OptInPush (verb first) + method.name = method.name + prefix[0].upper() + prefix[1:] + else: + # Default: prefix normally: "foo" → "PushFoo" + method.name = prefix.capitalize() + method.name[0].upper() + method.name[1:] + # Remove the getter method on the parent that returned the child + for parent_cls in namespace_classes[parent_ns]: + parent_cls.methods = [ + m + for m in parent_cls.methods + if not (m.is_getter and m.return_type.rstrip("?") in class_to_ns) + ] + namespace_classes[parent_ns].extend(namespace_classes[child_ns]) + del namespace_classes[child_ns] + + # ------------------------------------------------------------------ + # Pre-init property inference from setter methods + # ------------------------------------------------------------------ + + def _infer_pre_init_properties( + self, + api: DartPackageAPI, + plan: GenerationPlan, + control_name: str, + ) -> None: + """Infer properties from pre-init setter methods. + + Detects patterns like: + - ``consentRequired(bool)`` → ``require_consent: bool`` + - ``Debug.setLogLevel(OSLogLevel)`` → ``log_level: int`` + - ``Debug.setAlertLevel(OSLogLevel)`` → ``visual_alert_level: int`` + + These become control properties read at Dart init time, before + the SDK ``initialize()`` call. + """ + enum_names = {e.name for e in api.enums} + + for cls in api.classes: + for method in cls.methods: + # Must be void + single non-callback param + if method.return_type not in ("void", "Future"): + continue + if len(method.params) != 1: + continue + param = method.params[0] + if "Function" in param.dart_type or "Callback" in param.dart_type: + continue + + prop_name: str | None = None + prop_type: str = "" + prop_default: str = "None" + prop_doc: str = "" + + dart_pre_init_call = "" + + # Pattern: consentRequired(bool) → require_consent + if "consent" in method.name.lower() and "required" in method.name.lower(): + prop_name = "require_consent" + prop_type = "bool" + prop_default = "False" + prop_doc = "Whether consent is required before initializing." + sdk_accessor = self._compute_sdk_accessor( + cls.name, control_name, plan.dart_main_class + ) + dart_pre_init_call = f"if ({{var}}) {{ {sdk_accessor}.{method.name}(true); }}" + + # Pattern: setLogLevel(EnumType) → log_level + elif method.name.startswith("set") and param.dart_type in enum_names: + raw_name = method.name[3:] + prop_name = camel_to_snake(raw_name) + # If method name contains "Alert" or "Visual", prefer + # "visual_alert_level" to match common SDK conventions + if "alert" in raw_name.lower() and "visual" not in prop_name: + prop_name = f"visual_{prop_name}" + enum_type = param.dart_type + prop_type = f"{enum_type} | None" + prop_default = "None" + prop_doc = method.docstring or f"The {prop_name.replace('_', ' ')}." + sdk_accessor = self._compute_sdk_accessor( + cls.name, control_name, plan.dart_main_class + ) + dart_pre_init_call = ( + f"if ({{var}} != null) {{ " + f"{sdk_accessor}.{method.name}" + f"({enum_type}.values[{{var}}.value]); }}" + ) + + if prop_name and not any(p.python_name == prop_name for p in plan.properties): + plan.properties.append( + PropertyPlan( + python_name=prop_name, + python_type=prop_type, + default_value=prop_default, + docstring=prop_doc, + dart_pre_init_call=dart_pre_init_call, + ) + ) + + # ------------------------------------------------------------------ + # Main class processing + # ------------------------------------------------------------------ + + def _process_main_class( + self, + cls: DartClass, + plan: GenerationPlan, + dart_main_class: str, + api: DartPackageAPI | None = None, + ) -> None: + """Extract events, properties, and methods from a main class.""" + # Collect property names to skip methods already converted to properties + prop_methods = set() + for method in cls.methods: + if method.name == "initialize": + prop_methods.add(method.name) + elif "consent" in method.name.lower() and "required" in method.name.lower(): + prop_methods.add(method.name) + + for method in cls.methods: + # Skip methods that were converted to properties + if method.name in prop_methods: + continue + + # Skip internal Dart methods (hashCode, fromJson, etc.) + if method.name in _INTERNAL_METHODS: + continue + + # Check if this method is an event listener + event = self._detect_event(method, plan.control_name, api, dart_main_class, cls.name) + if event: + if not any(e.python_attr_name == event.python_attr_name for e in plan.events): + plan.events.append(event) + continue + + # Detect Stream getter as event source (e.g., onBatteryStateChanged) + stream_event = self._detect_stream_event(method, plan.control_name, api) + if stream_event: + if not any( + e.python_attr_name == stream_event.python_attr_name for e in plan.events + ): + plan.events.append(stream_event) + continue + + # Skip remove*Listener/Observer methods + if self._is_remove_listener_method(method): + continue + + # Skip methods with callback params (can't serialize over wire) + if self._has_callback_params(method): + continue + + # Skip setter methods that were converted to properties + if self._is_property_setter(method, plan): + continue + + # Check if this is a synchronous getter -> property + # Async getters (Future) become methods, not properties, + # because they require an await call to Flutter. + if method.is_getter and not method.params and not method.is_async: + prop = PropertyPlan( + python_name=camel_to_snake(method.name), + python_type=map_dart_type(method.return_type), + default_value=_default_for_type(map_dart_type(method.return_type)), + docstring=method.docstring, + ) + if not any(p.python_name == prop.python_name for p in plan.properties): + plan.properties.append(prop) + continue + + # Regular method + method_plan = self._build_method_plan(method, "", cls.name) + if not any(m.python_name == method_plan.python_name for m in plan.main_methods): + plan.main_methods.append(method_plan) + + # ------------------------------------------------------------------ + # Sub-module building + # ------------------------------------------------------------------ + + def _build_sub_module( + self, + ns_name: str, + classes: list[DartClass], + control_name: str, + dart_main_class: str, + plan: GenerationPlan | None = None, + ) -> SubModulePlan | None: + """Build a SubModulePlan from namespace classes.""" + class_name = f"{control_name}{ns_name.replace('_', ' ').title().replace(' ', '')}" + dart_prefix = ns_name + + methods: list[MethodPlan] = [] + docstrings: list[str] = [] + dart_class_name = "" + dart_sdk_accessor = "" + + for cls in classes: + if cls.docstring: + docstrings.append(cls.docstring) + if not dart_class_name: + dart_class_name = cls.name + dart_sdk_accessor = self._compute_sdk_accessor( + cls.name, control_name, dart_main_class + ) + for method in cls.methods: + # Skip internal Dart methods (hashCode, fromJson, etc.) + if method.name in _INTERNAL_METHODS: + continue + # Skip event listeners (handled at main level) + if self._is_listener_method(method): + continue + # Skip remove*Listener/Observer methods + if self._is_remove_listener_method(method): + continue + # Skip methods with callback params + if self._has_callback_params(method): + continue + # Skip setter methods that were converted to properties + # (but keep them in namespace sub-modules for runtime use) + if plan and self._is_property_setter(method, plan, in_namespace=True): + pass # Keep in namespace as method + method_plan = self._build_method_plan(method, dart_prefix, cls.name) + if not any(m.python_name == method_plan.python_name for m in methods): + methods.append(method_plan) + + if not methods: + return None + + return SubModulePlan( + module_name=ns_name, + class_name=class_name, + dart_prefix=dart_prefix, + methods=methods, + docstring=( + " ".join(docstrings) if docstrings else f"{control_name} {ns_name} namespace." + ), + dart_class_name=dart_class_name, + dart_sdk_accessor=dart_sdk_accessor, + ) + + # ------------------------------------------------------------------ + # Method building + # ------------------------------------------------------------------ + + def _build_method_plan( + self, + method: DartMethod, + dart_prefix: str, + dart_class_name: str, + ) -> MethodPlan: + """Convert a DartMethod to a MethodPlan.""" + python_name = camel_to_snake(method.name) + + # Invoke key = "{module_name}_{python_method_name}" — no abbreviation + if dart_prefix: + dart_method_name = f"{dart_prefix}_{python_name}" + else: + dart_method_name = python_name + + # Normalize common naming patterns for Pythonic API + python_name, dart_method_name = _normalize_method_name( + python_name, + dart_method_name, + method, + dart_prefix, + ) + + python_return, _dart_is_async = map_return_type(method.return_type) + # Sanitize return type: replace unknown Dart classes with dict | None + python_return = _sanitize_python_type(python_return, self._known_types) + + params: list[ParamPlan] = [] + for p in method.params: + python_type = map_dart_type(p.dart_type) + # Sanitize: replace unknown Dart classes with dict | None + python_type = _sanitize_python_type(python_type, self._known_types) + raw_name = camel_to_snake(p.name) if p.name != p.name.lower() else p.name + # Normalize param name: strip namespace words, simplify redundant names + param_name = _normalize_param_name(raw_name) + params.append( + ParamPlan( + python_name=param_name, + python_type=python_type, + dart_name=p.name, + dart_type=p.dart_type, + is_optional=not p.required and p.named, + is_named=p.named, + default=p.default, + docstring=p.docstring, + ) + ) + + # Sort params: required first, then optional (Python requirement) + params.sort(key=lambda p: (p.is_optional, 0)) + + # ALL methods are forced async because they call _invoke_method + return MethodPlan( + python_name=python_name, + dart_method_name=dart_method_name, + params=params, + return_type=python_return, + docstring=method.docstring, + is_async=True, + is_getter=method.is_getter, + dart_original_name=method.name, + dart_class_name=dart_class_name, + ) + + # ------------------------------------------------------------------ + # Stream-based event detection + # ------------------------------------------------------------------ + + def _detect_stream_event( + self, + method: DartMethod, + control_name: str, + api: DartPackageAPI | None, + ) -> EventPlan | None: + """Detect Stream methods/getters as event sources. + + Flutter packages commonly expose event sources as either: + - ``Stream get onXxxChanged`` (getter with "on" prefix) + - ``Stream getPositionStream(...)`` (method returning Stream) + + These are converted to ``on_xxx`` event handlers on the Python side. + """ + if not method.return_type.startswith("Stream"): + return None + + # Accept: getters starting with "on", OR any method returning Stream + is_on_getter = method.is_getter and method.name.startswith("on") + is_stream_method = not method.is_getter and "Stream" in method.return_type + if not is_on_getter and not is_stream_method: + return None + + # Extract event type from Stream + stream_match = re.match(r"Stream<(\w+)\??>?", method.return_type) + event_type_name = stream_match.group(1) if stream_match else "dynamic" + + # Build event name + if is_on_getter: + # onBatteryStateChanged → on_battery_state_changed + python_attr = camel_to_snake(method.name) + else: + # getPositionStream → on_position_stream + # getServiceStatusStream → on_service_status_stream + snake = camel_to_snake(method.name) + # Remove get_ prefix and _stream suffix for cleaner names + clean = snake.removeprefix("get_").removesuffix("_stream") + python_attr = f"on_{clean}" + + if not python_attr.startswith("on_"): + python_attr = f"on_{python_attr}" + dart_event_name = python_attr.removeprefix("on_") + + # Derive event class name + prefix = "OS" if control_name.lower().startswith("one") else control_name + event_class_name = f"{prefix}{event_type_name}Event" + if event_type_name.endswith("Event"): + event_class_name = f"{prefix}{event_type_name}" + + # Extract fields from helper class if available + fields: list[tuple[str, str]] = [("value", "str")] + if api: + for helper_cls in api.helper_classes: + if helper_cls.name == event_type_name: + fields = self._extract_fields_from_helper(helper_cls, api) + break + + return EventPlan( + python_attr_name=python_attr, + event_class_name=event_class_name, + dart_event_name=dart_event_name, + fields=fields, + dart_listener_method=method.name, + dart_sdk_accessor="", + ) + + # ------------------------------------------------------------------ + # Event detection (listener pattern) + # ------------------------------------------------------------------ + + def _detect_event( + self, + method: DartMethod, + control_name: str, + api: DartPackageAPI | None, + dart_main_class: str, + source_class_name: str, + ) -> EventPlan | None: + """Detect if a method registers an event listener.""" + name = method.name + + # Patterns: addXxxListener, addXxxObserver, addXxxHandler + # Also: addListener, addObserver (empty core) + listener_match = re.match(r"add(\w*?)(Listener|Observer|Handler)$", name) + if not listener_match: + return None + + event_core = listener_match.group(1) + + # Build a meaningful event name with namespace prefix + sdk_accessor = self._compute_sdk_accessor(source_class_name, control_name, dart_main_class) + # Determine namespace prefix for event naming + ns_prefix = self._event_namespace_prefix(source_class_name, control_name) + + # If event_core is empty (e.g., addObserver), derive from context + if not event_core: + event_core = self._derive_event_core_from_context( + method, source_class_name, control_name, api + ) + else: + # Enrich event_core from typedef name if it provides more info + # E.g., method "addPermissionObserver" → core "Permission" + # but typedef "OnNotificationPermissionChangeObserver" → "PermissionChange" + event_core = self._enrich_event_core(event_core, method, api) + + snake = camel_to_snake(event_core) if event_core else "change" + + # Shorten verbose event names: + # "foreground_will_display" → "foreground" (will_display is implied) + snake = _shorten_event_name(snake) + + # Deduplicate: if snake already starts with the namespace prefix, + # strip it to avoid "on_user_user_change" → "on_user_change". + if ns_prefix and snake.startswith(ns_prefix + "_"): + snake = snake[len(ns_prefix) + 1 :] + elif ns_prefix and snake.startswith(ns_prefix): + snake = snake[len(ns_prefix) :].lstrip("_") or "change" + + if ns_prefix: + python_attr = f"on_{ns_prefix}_{snake}" + dart_event_name = f"{ns_prefix}_{snake}" + else: + python_attr = f"on_{snake}" + dart_event_name = snake + + # Try to extract the event type name from callback parameter + event_class_name = self._derive_event_class_name(method, control_name, event_core, api) + + # Extract event fields from helper classes if available + fields = self._extract_event_fields(method, api) + + return EventPlan( + python_attr_name=python_attr, + event_class_name=event_class_name, + dart_event_name=dart_event_name, + fields=fields, + dart_listener_method=name, + dart_sdk_accessor=sdk_accessor, + ) + + def _enrich_event_core( + self, + event_core: str, + method: DartMethod, + api: DartPackageAPI | None, + ) -> str: + """Enrich event_core from typedef/callback type name. + + If the method name gives a short core like "Permission", the typedef + name ``OnNotificationPermissionChangeObserver`` provides the richer + "PermissionChange". + """ + if not api or not method.params: + return event_core + + cb_type = method.params[0].dart_type + # Look for On{Prefix}{EventCore}{Extra}(Observer|Listener|Handler) pattern + # The non-capturing group MUST match (required prefix between On and event_core) + # This prevents matching OnClickInAppMessageListener with event_core="Click" + # but DOES match OnNotificationPermissionChangeObserver with event_core="Permission" + m = re.match( + r"On[A-Z]\w+?(" + re.escape(event_core) + r"\w*?)" + r"(Observer|Listener|Handler|Change)$", + cb_type, + ) + if m: + enriched = m.group(1) + suffix = m.group(2) + if suffix == "Change" and not enriched.endswith("Change"): + enriched += "Change" + if len(enriched) > len(event_core): + return enriched + + return event_core + + def _derive_event_core_from_context( + self, + method: DartMethod, + source_class_name: str, + control_name: str, + api: DartPackageAPI | None, + ) -> str: + """Derive event core name when the listener method has no descriptive name. + + For example: ``OneSignalUser.addObserver`` → event core "Change" + (derived from callback type ``OnUserChangeObserver``). + """ + # Try to extract from callback parameter typedef name + if method.params: + param_type = method.params[0].dart_type + # Pattern: On{Something}Observer/Listener → extract {Something} + m = re.match(r"On(\w+?)(Observer|Listener|Handler|Change)$", param_type) + if m: + return m.group(1) + (m.group(2) if m.group(2) == "Change" else "") + + # Try typedef resolution + if api and param_type in api.typedefs: + resolved_type = api.typedefs[param_type] + # Look for Change/Changed in the resolved type name + m2 = re.match(r"\w*?(Change\w*|Changed\w*)$", resolved_type) + if m2: + return m2.group(1) + + # Fallback: derive from class name + suffix = source_class_name[len(control_name) :] + if suffix: + return suffix + "Change" + return "Change" + + def _event_namespace_prefix(self, source_class_name: str, control_name: str) -> str: + """Compute a short prefix for event names based on source class. + + OneSignalNotifications -> "notification" + OneSignalInAppMessages -> "iam" + OneSignal (main) -> "" (no prefix) + """ + if source_class_name.lower() == control_name.lower(): + return "" + suffix = source_class_name[len(control_name) :] + if not suffix: + return "" + snake = camel_to_snake(suffix) + # Common abbreviations + abbreviations = { + "notifications": "notification", + "in_app_messages": "iam", + "push_subscription": "push_subscription", + } + return abbreviations.get(snake, snake) + + def _derive_event_class_name( + self, + method: DartMethod, + control_name: str, + event_core: str, + api: DartPackageAPI | None = None, + ) -> str: + """Derive a proper event class name. + + Always returns names ending in ``Event``. If the Dart SDK uses + a ``*State``/``*ChangedState`` type for its observer callback, the + generated Python event class is still named ``*Event`` for + consistency. + """ + # Check callback parameter for a type hint + if method.params: + cb_type = method.params[0].dart_type + + # Direct match: parameter type already contains "Event" + type_match = re.search(r"\b([A-Z]\w+Event)\b", cb_type) + if type_match: + return type_match.group(1) + + # Resolve typedef to find event type + if api and cb_type in api.typedefs: + resolved = api.typedefs[cb_type] + type_match = re.search(r"\b([A-Z]\w+)\b", resolved) + if type_match: + resolved_name = type_match.group(1) + # Normalize *State → *Event + return _normalize_event_class_name(resolved_name) + + # Generate name: OS + EventCore + Event + prefix = "OS" if control_name.lower().startswith("one") else control_name + return f"{prefix}{event_core}Event" + + def _extract_event_fields( + self, + method: DartMethod, + api: DartPackageAPI | None, + ) -> list[tuple[str, str]]: + """Extract typed event fields from the callback's event type class.""" + if not api or not method.params: + return [("data", "dict")] + + cb_type = method.params[0].dart_type + + # Resolve typedef first + actual_type = cb_type + if cb_type in api.typedefs: + actual_type = api.typedefs[cb_type] + + # For simple/primitive callback types (e.g., bool for permission change) + if actual_type in ("bool", "bool?"): + return [("value", "bool")] + if actual_type in ("String", "String?"): + return [("value", "str")] + if actual_type in ("int", "int?"): + return [("value", "int")] + + # Try to find the event type in helper_classes + type_match = re.search(r"\b([A-Z]\w+)\b", actual_type) + if not type_match: + return [("data", "dict")] + + event_type_name = type_match.group(1) + + # Search helper_classes for matching type + for helper_cls in api.helper_classes: + if helper_cls.name == event_type_name: + return self._extract_fields_from_helper(helper_cls, api) + + return [("data", "dict")] + + def _extract_fields_from_helper( + self, + helper_cls: DartClass, + api: DartPackageAPI, + ) -> list[tuple[str, str]]: + """Extract fields from a helper class, following nested types. + + When a helper has a ``current`` field pointing to another helper, + the nested fields are expanded in-place so the Python event carries + the leaf-level data directly. + """ + fields: list[tuple[str, str]] = [] + + for m in helper_cls.methods: + if not m.is_getter or m.params: + continue + py_name = camel_to_snake(m.name) + py_type = map_dart_type(m.return_type) + dart_type_clean = m.return_type.rstrip("?") + + # If this field points to another helper class, expand it + nested_cls = None + for hc in api.helper_classes: + if hc.name == dart_type_clean: + nested_cls = hc + break + + if nested_cls and py_name in ("current", "previous"): + # Expand "current" inline; skip "previous" (redundant) + if py_name == "current": + nested_fields = self._extract_fields_from_helper(nested_cls, api) + if nested_fields and nested_fields != [("data", "dict")]: + fields.extend(nested_fields) + continue + + # Simplify complex types to basic Python types + if ( + py_type + not in ( + "str", + "int", + "float", + "bool", + "Any", + "str | None", + "int | None", + "float | None", + "bool | None", + ) + and not py_type.startswith("list") + and not py_type.startswith("dict") + ): + py_type = "dict" + fields.append((py_name, py_type)) + + if fields: + return fields + + return [("data", "dict")] + + # ------------------------------------------------------------------ + # Re-exported type generation + # ------------------------------------------------------------------ + + def _generate_reexported_types( + self, + api: DartPackageAPI, + plan: GenerationPlan, + generated_type_names: set[str], + ) -> None: + """Generate stub types for re-exported types from platform_interface. + + Uses naming heuristics to determine if a re-exported type is an + enum, a data class, or an exception type, and generates appropriate + stubs so that generated code can reference them without NameError. + """ + # Collect all type names referenced in the plan + referenced = self._collect_referenced_types(plan) + + # Suffixes that indicate enum types + _ENUM_SUFFIXES = ( + "Type", + "State", + "Status", + "Mode", + "Level", + "Source", + "Device", + "Permission", + "Accuracy", + "Direction", + "Priority", + "Visibility", + "Setting", # singular (e.g. AppleNotificationSetting) + ) + # Suffixes that indicate data/params classes + _DATA_SUFFIXES = ( + "Params", + "Result", + "Options", + "Configuration", + "Config", + "Settings", # plural (e.g. NotificationSettings) + "Info", + "Response", + "Position", + "Data", + "File", + "Message", + "Notification", + "Sound", + ) + # Suffixes to skip (widget/callback types not useful on Python side) + _SKIP_SUFFIXES = ( + "Builder", + "Delegate", + "Callback", + "Handler", + "Link", + "Target", + ) + + for type_name, _source_pkg in api.reexported_types.items(): + if type_name in generated_type_names: + continue # Already generated from local source + if type_name.startswith("_"): + continue + if any(type_name.endswith(s) for s in _SKIP_SUFFIXES): + continue + # Only generate types that are actually referenced + if type_name not in referenced and type_name not in self._known_types: + continue + + if any(type_name.endswith(s) for s in _ENUM_SUFFIXES): + # Generate as enum with a string value fallback + plan.enums.append( + EnumPlan( + python_name=type_name, + values=[("UNKNOWN", "unknown", "")], + docstring=( + f"{type_name} enum (stub — values should be filled " + f"from the {_source_pkg} platform interface)." + ), + ) + ) + generated_type_names.add(type_name) + elif any(type_name.endswith(s) for s in _DATA_SUFFIXES): + # Generate as a stub dataclass in types.py + plan.stub_data_classes.append( + StubDataClass( + python_name=type_name, + fields=[("data", "dict")], + docstring=( + f"{type_name} data class (stub — fields should be " + f"filled from the {_source_pkg} platform interface)." + ), + ) + ) + generated_type_names.add(type_name) + elif type_name in referenced: + # Fallback: generate as enum stub for any referenced type + # that didn't match known suffixes (e.g. LocationPermission, + # ImageSource, XFile). This prevents NameError at runtime. + plan.enums.append( + EnumPlan( + python_name=type_name, + values=[("UNKNOWN", "unknown", "")], + docstring=( + f"{type_name} enum (stub — values should be filled " + f"from the {_source_pkg} platform interface)." + ), + ) + ) + generated_type_names.add(type_name) + + def _generate_local_data_classes( + self, + api: DartPackageAPI, + plan: GenerationPlan, + generated_type_names: set[str], + ) -> None: + """Generate Python dataclasses from local helper classes. + + Detects helper classes (Configuration, Options, Params, etc.) + that are referenced as method parameter types and generates + proper Python dataclasses with their parsed fields. + """ + referenced = self._collect_referenced_types(plan) + + # Data class suffixes (these helper classes have useful fields) + _DATA_SUFFIXES = ( + "Configuration", + "Config", + "Settings", + "Options", + "Params", + "Info", + "Result", + "Response", + "Position", + "Data", + "File", + ) + # Event suffixes should not be generated as data classes + _SKIP_SUFFIXES = ("Event", "State", "ChangedState") + + for helper_cls in api.helper_classes: + name = helper_cls.name + if name in generated_type_names: + continue + if name.startswith("_"): + continue + if any(name.endswith(s) for s in _SKIP_SUFFIXES): + continue + if not any(name.endswith(s) for s in _DATA_SUFFIXES): + continue + # Only generate if actually referenced by methods + if name not in referenced: + continue + + # Extract fields from the helper class methods (which includes + # parsed instance fields when skip_filter=False) + fields: list[tuple[str, str]] = [] + for method in helper_cls.methods: + if method.is_getter and not method.params: + # Skip constructor entries (name matches class name) + if method.name == name: + continue + field_name = camel_to_snake(method.name) + field_type = map_dart_type(method.return_type) + fields.append((field_name, field_type)) + + if not fields: + fields = [("data", "dict")] + + plan.stub_data_classes.append( + StubDataClass( + python_name=name, + fields=fields, + docstring=helper_cls.docstring or f"{name} configuration.", + ) + ) + generated_type_names.add(name) + + def _collect_referenced_types(self, plan: GenerationPlan) -> set[str]: + """Collect all type names referenced by methods, params, and return types.""" + refs: set[str] = set() + all_methods = list(plan.main_methods) + for sub in plan.sub_modules: + all_methods.extend(sub.methods) + for method in all_methods: + # Check return type + base = method.return_type.split("[")[0].split("|")[0].strip() + if base and base[0].isupper(): + refs.add(base) + for p in method.params: + base = p.python_type.split("[")[0].split("|")[0].strip() + if base and base[0].isupper(): + refs.add(base) + base_dart = p.dart_type.rstrip("?") + if base_dart and base_dart[0].isupper(): + refs.add(base_dart) + return refs + + # ------------------------------------------------------------------ + # Helper checks + # ------------------------------------------------------------------ + + def _is_listener_method(self, method: DartMethod) -> bool: + """Check if a method is a listener registration.""" + return bool(re.match(r"add\w*(Listener|Observer|Handler)$", method.name)) + + def _is_remove_listener_method(self, method: DartMethod) -> bool: + """Check if a method is a listener removal.""" + return bool(re.match(r"remove\w*(Listener|Observer|Handler)$", method.name)) + + def _has_callback_params(self, method: DartMethod) -> bool: + """Check if any params are callback/function types (not serializable).""" + for p in method.params: + t = p.dart_type + if "Function" in t or "Callback" in t or "void " in t: + return True + return False + + def _is_property_setter( + self, method: DartMethod, plan: GenerationPlan, *, in_namespace: bool = False + ) -> bool: + """Check if a setter method was converted to a property. + + When ``in_namespace`` is True, the setter is kept as a method + (for runtime use from the sub-module) and NOT skipped. + """ + if in_namespace: + return False # Keep setters as methods in sub-modules + if not method.name.startswith("set") or len(method.params) != 1: + return False + raw_name = method.name[3:] + prop_name = camel_to_snake(raw_name) + return any(p.python_name == prop_name for p in plan.properties) + + +def _shorten_event_name(snake: str) -> str: + """Shorten verbose event names. + + ``foreground_will_display`` → ``foreground`` + (the "will_display"/"did_display" part is implied by the event handler.) + """ + # Remove trailing lifecycle suffixes that are already implied + for suffix in ("_will_display", "_did_display", "_will_dismiss", "_did_dismiss"): + if snake.endswith(suffix) and len(snake) > len(suffix): + return snake[: -len(suffix)] + return snake + + +def _normalize_event_class_name(name: str) -> str: + """Normalize a Dart type name to a Python event class name. + + Ensures the name ends with ``Event``. Replaces ``*State``/``*ChangedState`` + suffixes with ``Event``. + """ + if name.endswith("Event"): + return name + if name.endswith("ChangedState"): + return name[: -len("ChangedState")] + "ChangedEvent" + if name.endswith("State"): + return name[: -len("State")] + "Event" + return name + "Event" + + +def _normalize_param_name(param_name: str) -> str: + """Normalize parameter names — keep original Dart names (camel_to_snake). + + Minimal normalization: the direct ``camel_to_snake`` mapping from Dart + parameter names is usually the best Python name. + """ + return param_name + + +def _normalize_method_name( + python_name: str, + dart_method_name: str, + method: DartMethod, + dart_prefix: str, +) -> tuple[str, str]: + """Normalize method naming for Pythonic conventions. + + Applies minimal patterns: + - Bare getter methods (no params, non-void return) → add ``get_`` or ``is_`` prefix + - Otherwise keep the direct ``camel_to_snake`` mapping from Dart + """ + + def _rebuild_dart_key(new_name: str) -> str: + if dart_prefix: + return f"{dart_prefix}_{new_name}" + return new_name + + # Verb prefixes that already convey meaning — don't add get_/is_ on top + _VERB_PREFIXES = ("get_", "is_", "are_", "can_", "has_", "should_", "does_", "will_", "was_") + + # Bare getter: no params, non-void return, no existing verb prefix → add "get_" or "is_" + if ( + method.is_getter + and not method.params + and method.return_type not in ("void", "Future") + and not any(python_name.startswith(vp) for vp in _VERB_PREFIXES) + ): + is_bool = method.return_type in ("bool", "bool?", "Future", "Future") + prefix_word = "is_" if is_bool else "get_" + python_name = f"{prefix_word}{python_name}" + dart_method_name = _rebuild_dart_key(python_name) + + return python_name, dart_method_name + + +# Suffixes that strongly indicate a Dart enum type. When an unknown type +# ends with one of these, ``_sanitize_python_type`` maps it to ``str`` +# (serialised as a string on the wire) instead of ``dict | None``. +# This handles enums from transitive dependencies that the parser never +# downloads (e.g. ``RiveHitTestBehavior`` from ``rive_native``). +_ENUM_LIKE_SUFFIXES = ( + "Behavior", + "Behaviour", + "Mode", + "Style", + "Kind", + "Fit", + "Order", + "Level", + "Direction", + "Action", + "Axis", + "Clip", + "Curve", + "Cap", + "Join", + "Policy", + "Strategy", + "Type", + "State", +) + +# Python types that are valid and should not be replaced +_KNOWN_PYTHON_TYPES = frozenset( + { + "str", + "int", + "float", + "bool", + "bytes", + "None", + "Any", + "list", + "dict", + "set", + "tuple", + "Optional", + } +) + + +def _sanitize_python_type( + python_type: str, + known_types: frozenset[str] = frozenset(), +) -> str: + """Replace unknown Dart class types with ``dict | None``. + + Types like ``LiveActivitySetupOptions`` that pass through the + type_map unchanged are replaced with a safe fallback, since + they won't exist in the generated Python code. + + *known_types* contains re-exported type names from the package + barrel file that should be preserved (e.g. ``BiometricType``). + """ + # Strip nullable suffix for checking (use proper substring match, + # not rstrip which removes individual characters). + is_nullable = python_type.endswith("| None") or python_type.endswith("|None") + if is_nullable: + base = re.sub(r"\s*\|\s*None$", "", python_type).strip() + else: + base = python_type.strip() + + # Generic types (list[...], dict[...]) — sanitize inner types recursively + _KNOWN_GENERIC_OUTERS = {"list", "dict", "set", "tuple", "frozenset", "optional"} + if "[" in python_type: + bracket_start = python_type.index("[") + outer = python_type[:bracket_start].strip() + # Unknown generic wrappers (Factory, ValueChanged, etc.) → dict | None + if outer.lower() not in _KNOWN_GENERIC_OUTERS: + return "dict | None" + inner = python_type[bracket_start + 1 : python_type.rindex("]")] + sanitized_inner = _sanitize_python_type(inner, known_types) + return f"{outer}[{sanitized_inner}]" + + # Known Python types are fine + if base.lower() in {t.lower() for t in _KNOWN_PYTHON_TYPES}: + return python_type + + # Types starting with OS are enum references — keep them + if base.startswith("OS") or base.startswith("os"): + return python_type + + # Re-exported public API types — keep them + if base in known_types: + return python_type + + # If the type name ends with a common enum suffix, treat as str + # (enums from transitive deps that weren't downloaded/parsed). + if base and base[0].isupper() and base.endswith(_ENUM_LIKE_SUFFIXES): + return "str | None" if is_nullable else "str" + + # If it looks like a Dart class name (UpperCase), replace with dict + if base and base[0].isupper() and base.isidentifier(): + return "dict | None" if is_nullable else "dict" + + return python_type + + +def _default_for_type(python_type: str) -> str: + """Return a sensible default value string for a Python type.""" + if python_type == "str": + return '""' + if python_type == "bool": + return "False" + if python_type == "int": + return "0" + if python_type == "float": + return "0.0" + if "None" in python_type: + return "None" + if python_type.startswith("list"): + return "field(default_factory=list)" + if python_type.startswith("dict"): + return "field(default_factory=dict)" + return "None" + + +def _post_sanitize_property_types( + plan: "GenerationPlan", + generated_names: set[str], +) -> None: + """Re-sanitize property types against actually-generated type names. + + Types from barrel analysis (``known_types``) that were NOT generated as + Python classes/enums are replaced with ``dict | None``. + """ + # Build the set of types that are valid in generated Python code + # Use lowercase for case-insensitive comparison (see _resanitize_type) + valid = {n.lower() for n in generated_names} + valid.update(t.lower() for t in _KNOWN_PYTHON_TYPES) + # Flet types (ft.Color, ft.Alignment, etc.) are always valid + flet_prefixes = ("ft.",) + + for prop in plan.properties: + prop.python_type = _resanitize_type(prop.python_type, valid, flet_prefixes) + + # Also sanitize method param types and return types + all_methods = list(plan.main_methods) + for sub in plan.sub_modules: + all_methods.extend(sub.methods) + + for method in all_methods: + method.return_type = _resanitize_type(method.return_type, valid, flet_prefixes) + for p in method.params: + p.python_type = _resanitize_type(p.python_type, valid, flet_prefixes) + + +def _resanitize_type( + python_type: str, + valid_types: set[str], + flet_prefixes: tuple[str, ...], +) -> str: + """Replace undefined class references in a type annotation.""" + is_nullable = python_type.endswith("| None") or python_type.endswith("|None") + if is_nullable: + base = re.sub(r"\s*\|\s*None$", "", python_type).strip() + else: + base = python_type.strip() + + # Generic types — recurse into inner + if "[" in base: + bracket_start = base.index("[") + outer = base[:bracket_start].strip() + inner = base[bracket_start + 1 : base.rindex("]")] + sanitized_inner = _resanitize_type(inner, valid_types, flet_prefixes) + result = f"{outer}[{sanitized_inner}]" + if is_nullable: + return f"{result} | None" + return result + + # Known Python builtins, Flet types, generated types — keep + if base.lower() in valid_types: + return python_type + if any(base.startswith(p) for p in flet_prefixes): + return python_type + + # Unknown UpperCase type → dict | None + if base and base[0].isupper() and base.isidentifier(): + return "dict | None" + + return python_type diff --git a/src/flet_pkg/core/downloader.py b/src/flet_pkg/core/downloader.py new file mode 100644 index 0000000..7a7131e --- /dev/null +++ b/src/flet_pkg/core/downloader.py @@ -0,0 +1,136 @@ +""" +Pub.dev package downloader. + +Downloads Flutter package source tarballs from pub.dev with +local caching to avoid repeated downloads. +""" + +import tarfile +from dataclasses import dataclass +from pathlib import Path + +import httpx + +from flet_pkg.ui.console import console + + +class PackageNotFoundError(Exception): + """Raised when a package is not found on pub.dev.""" + + +class DownloadError(Exception): + """Raised when a download fails.""" + + +@dataclass +class PackageMetadata: + """Metadata for a pub.dev package.""" + + name: str + version: str + description: str = "" + homepage: str = "" + repository: str = "" + + +class PubDevDownloader: + """Downloads and caches Flutter packages from pub.dev. + + Packages are cached in ``~/.cache/flet-pkg/{name}-{version}/`` + so repeated runs are instantaneous. + """ + + PUB_API = "https://pub.dev/api/packages/{name}" + TARBALL_URL = "https://pub.dev/packages/{name}/versions/{version}.tar.gz" + CACHE_DIR = Path.home() / ".cache" / "flet-pkg" + + def __init__(self, cache_dir: Path | None = None): + self.cache_dir = cache_dir or self.CACHE_DIR + + def fetch_metadata(self, package_name: str) -> PackageMetadata: + """Fetch package metadata from pub.dev. + + Args: + package_name: The pub.dev package name. + + Returns: + PackageMetadata with name, version, description, etc. + + Raises: + PackageNotFoundError: If the package doesn't exist. + DownloadError: On network errors. + """ + url = self.PUB_API.format(name=package_name) + try: + response = httpx.get(url, follow_redirects=True, timeout=30) + except httpx.HTTPError as e: + raise DownloadError(f"Failed to fetch metadata for '{package_name}': {e}") from e + + if response.status_code == 404: + raise PackageNotFoundError(f"Package '{package_name}' not found on pub.dev.") + if response.status_code != 200: + raise DownloadError( + f"Unexpected status {response.status_code} fetching '{package_name}'." + ) + + data = response.json() + latest = data.get("latest", {}).get("pubspec", {}) + return PackageMetadata( + name=package_name, + version=data.get("latest", {}).get("version", ""), + description=latest.get("description", ""), + homepage=latest.get("homepage", ""), + repository=latest.get("repository", ""), + ) + + def download(self, package_name: str, version: str | None = None) -> Path: + """Download a Flutter package source tarball from pub.dev. + + Args: + package_name: The pub.dev package name. + version: Specific version to download. If None, uses latest. + + Returns: + Path to the extracted package directory. + + Raises: + PackageNotFoundError: If the package doesn't exist. + DownloadError: On network/extraction errors. + """ + if version is None: + metadata = self.fetch_metadata(package_name) + version = metadata.version + + cache_path = self.cache_dir / f"{package_name}-{version}" + if cache_path.exists() and (cache_path / "lib").exists(): + return cache_path + + url = self.TARBALL_URL.format(name=package_name, version=version) + with console.status(f"[info]Downloading {package_name} v{version}...[/info]"): + try: + response = httpx.get(url, follow_redirects=True, timeout=60) + except httpx.HTTPError as e: + raise DownloadError(f"Failed to download '{package_name}': {e}") from e + + if response.status_code == 404: + raise PackageNotFoundError( + f"Package '{package_name}' version '{version}' not found." + ) + if response.status_code != 200: + raise DownloadError( + f"Unexpected status {response.status_code} downloading '{package_name}'." + ) + + cache_path.mkdir(parents=True, exist_ok=True) + tarball_path = cache_path / "package.tar.gz" + tarball_path.write_bytes(response.content) + + try: + with tarfile.open(tarball_path, "r:gz") as tar: + tar.extractall(cache_path, filter="data") + except tarfile.TarError as e: + raise DownloadError(f"Failed to extract '{package_name}': {e}") from e + finally: + tarball_path.unlink(missing_ok=True) + + return cache_path diff --git a/src/flet_pkg/core/generators/__init__.py b/src/flet_pkg/core/generators/__init__.py new file mode 100644 index 0000000..835c2ee --- /dev/null +++ b/src/flet_pkg/core/generators/__init__.py @@ -0,0 +1,15 @@ +"""Code generators for Flet extension packages.""" + +from flet_pkg.core.generators.dart_service import DartServiceGenerator +from flet_pkg.core.generators.python_control import PythonControlGenerator +from flet_pkg.core.generators.python_init import PythonInitGenerator +from flet_pkg.core.generators.python_submodule import PythonSubModuleGenerator +from flet_pkg.core.generators.python_types import PythonTypesGenerator + +__all__ = [ + "DartServiceGenerator", + "PythonControlGenerator", + "PythonInitGenerator", + "PythonSubModuleGenerator", + "PythonTypesGenerator", +] diff --git a/src/flet_pkg/core/generators/base.py b/src/flet_pkg/core/generators/base.py new file mode 100644 index 0000000..7e6b7e9 --- /dev/null +++ b/src/flet_pkg/core/generators/base.py @@ -0,0 +1,92 @@ +"""Base class for code generators.""" + +from abc import ABC, abstractmethod + +from flet_pkg.core.models import GenerationPlan + + +class CodeGenerator(ABC): + """Abstract base class for code generators. + + Each generator receives a ``GenerationPlan`` and produces a dict + mapping filenames to their generated content. + """ + + @abstractmethod + def generate(self, plan: GenerationPlan) -> dict[str, str]: + """Generate files from the plan. + + Returns: + Dict mapping relative filename to file content string. + """ + + @staticmethod + def _format_docstring(doc: str, indent: str = " ") -> list[str]: + """Format a docstring preserving multi-line structure. + + Args: + doc: Raw docstring text. + indent: Indentation prefix for each line. + + Returns: + List of formatted docstring lines, or empty list if *doc* is empty. + """ + if not doc: + return [] + doc_lines = doc.split("\n") + if len(doc_lines) == 1: + return [f'{indent}"""{doc}"""'] + result = [f'{indent}"""{doc_lines[0]}'] + for dl in doc_lines[1:]: + result.append(f"{indent}{dl}" if dl.strip() else "") + result.append(f'{indent}"""') + return result + + @staticmethod + def _py_default(dart_default: str | None) -> str: + """Convert a Dart default value to a Python literal. + + Args: + dart_default: Dart default value string (e.g. ``"true"``, ``"0.5"``). + + Returns: + Equivalent Python literal string (e.g. ``"True"``, ``"0.5"``). + """ + if not dart_default: + return "None" + _DART_TO_PYTHON = { + "true": "True", + "false": "False", + "null": "None", + } + if dart_default in _DART_TO_PYTHON: + return _DART_TO_PYTHON[dart_default] + + # Numeric literals pass through (must check BEFORE "." check + # since decimals like "0.0" contain dots) + try: + float(dart_default) + return dart_default + except ValueError: + pass + + # Dart constructor calls: `const SomeClass()`, `SomeClass()` + if "(" in dart_default: + return "None" + + # Dart enum references: `SomeEnum.value` (not numeric decimals) + if "." in dart_default: + return "None" + + # Dart const collections: `const []`, `const {}` + if dart_default.startswith("const "): + return "None" + + # String literals: keep as-is if they look like Python strings + if (dart_default.startswith('"') and dart_default.endswith('"')) or ( + dart_default.startswith("'") and dart_default.endswith("'") + ): + return dart_default + + # Unknown complex expression → None + return "None" diff --git a/src/flet_pkg/core/generators/dart_service.py b/src/flet_pkg/core/generators/dart_service.py new file mode 100644 index 0000000..b02b8d6 --- /dev/null +++ b/src/flet_pkg/core/generators/dart_service.py @@ -0,0 +1,904 @@ +"""Generator for the Dart FletService file. + +Produces the Dart service (or widget) implementation that handles +method dispatch from Python, event listener setup, and real SDK calls. +""" + +from flet_pkg.core.generators.base import CodeGenerator +from flet_pkg.core.models import ( + GenerationPlan, + MethodPlan, + SiblingWidgetPlan, + SubControlPlan, + SubModulePlan, +) +from flet_pkg.core.parser import camel_to_snake + + +class DartServiceGenerator(CodeGenerator): + """Generates the Dart FletService/FletWidget file.""" + + def generate(self, plan: GenerationPlan) -> dict[str, str]: + """Generate Dart service/widget files and optional extension.dart. + + Args: + plan: Generation plan produced by the analyzer. + + Returns: + Mapping of filename to generated Dart source code. + """ + control_snake = plan.control_name_snake or camel_to_snake(plan.control_name) + service_type = "Service" if plan.base_class == "ft.Service" else "Widget" + filename = f"{control_snake}_{service_type.lower()}.dart" + + # UI controls use StatefulWidget + LayoutControl pattern + if service_type == "Widget": + files: dict[str, str] = {filename: self._generate_ui_control(plan, control_snake)} + + # Generate sibling widget Dart files + for sibling in plan.sibling_widgets: + sib_snake = sibling.control_name_snake or camel_to_snake(sibling.control_name) + sib_file = f"{sib_snake}_widget.dart" + files[sib_file] = self._generate_sibling_widget(sibling, plan) + + # Generate extension.dart when siblings exist + if plan.sibling_widgets: + files["extension.dart"] = self._generate_extension_dart(plan) + + return files + + # Service path — unchanged + class_name = f"{plan.control_name}{service_type}" + lines: list[str] = [] + + # Imports + lines.append("import 'dart:convert';") + lines.append("import 'package:flet/flet.dart';") + lines.append("import 'package:flutter/foundation.dart';") + if plan.dart_import: + lines.append(f"import '{plan.dart_import}';") + lines.append("") + + # Class + dart_base = f"Flet{service_type}" + lines.append(f"/// {plan.control_name} {service_type.lower()} implementation for Flet.") + lines.append("///") + lines.append( + f"/// This {service_type.lower()} handles all communication between the Python SDK" + ) + lines.append(f"/// and the {plan.flutter_package} Flutter SDK.") + lines.append(f"class {class_name} extends {dart_base} {{") + lines.append(f" {class_name}({{required super.control}});") + lines.append("") + lines.append(" bool _initialized = false;") + lines.append(" bool _listenersSetup = false;") + lines.append("") + + # init() + lines.append(" @override") + lines.append(" void init() {") + lines.append(" super.init();") + lines.append(" control.addInvokeMethodListener(_onInvokeMethod);") + lines.append(f" _initialize{plan.control_name}();") + lines.append(" }") + lines.append("") + + # update() + lines.append(" @override") + lines.append(" void update() {") + lines.append(" super.update();") + lines.append(f" _initialize{plan.control_name}();") + lines.append(" }") + lines.append("") + + # dispose() + lines.append(" @override") + lines.append(" void dispose() {") + lines.append(" control.removeInvokeMethodListener(_onInvokeMethod);") + lines.append(" super.dispose();") + lines.append(" }") + lines.append("") + + # _initialize method with property reading + self._render_initialize(lines, plan, class_name) + + # _setupListeners with real event listeners + self._render_setup_listeners(lines, plan, class_name) + + # Enum parser helpers (if plan uses enums in methods) + self._render_enum_helpers(lines, plan) + + # _onInvokeMethod with switch dispatch + self._render_invoke_method(lines, plan) + + # Method implementations with real SDK calls + all_methods = list(plan.main_methods) + for sub in plan.sub_modules: + all_methods.extend(sub.methods) + + for method in all_methods: + sub_module = self._find_sub_module_for_method(method, plan) + lines.extend(self._render_dart_method(method, plan, sub_module)) + lines.append("") + + # _handleError + self._render_handle_error(lines, plan, class_name) + + lines.append("}") + lines.append("") + + return {filename: "\n".join(lines)} + + # ------------------------------------------------------------------ + # UI Control: StatefulWidget + LayoutControl + # ------------------------------------------------------------------ + + def _generate_ui_control(self, plan: GenerationPlan, control_snake: str) -> str: + """Generate a StatefulWidget Dart file for UI control extensions. + + Produces a ``StatefulWidget`` whose ``build()`` reads properties + using typed getters (``getAlignment``, ``getBoxFit``, etc.) and + wraps the SDK widget in a ``LayoutControl``. + + Args: + plan: Generation plan with widget properties and events. + control_snake: Snake-case control name for file references. + + Returns: + Complete Dart source code string. + """ + widget_class = f"{plan.control_name}Widget" + state_class = f"_{plan.control_name}WidgetState" + sdk_class = plan.dart_main_class or plan.control_name + lines: list[str] = [] + + # Imports + lines.append("import 'package:flet/flet.dart';") + # Use material.dart if TextStyle getters need Theme.of(context) + needs_material = any("Theme.of(context)" in (p.dart_getter or "") for p in plan.properties) + if needs_material: + lines.append("import 'package:flutter/material.dart';") + else: + lines.append("import 'package:flutter/widgets.dart';") + if plan.dart_import: + lines.append(f"import '{plan.dart_import}';") + lines.append("") + + # StatefulWidget class + lines.append(f"/// {plan.control_name} widget implementation for Flet.") + lines.append("///") + lines.append(f"/// Wraps the {plan.flutter_package} Flutter widget in a LayoutControl.") + lines.append(f"class {widget_class} extends StatefulWidget {{") + lines.append(" final Control control;") + lines.append(f" const {widget_class}({{super.key, required this.control}});") + lines.append("") + lines.append(" @override") + lines.append(f" State<{widget_class}> createState() => {state_class}();") + lines.append("}") + lines.append("") + + # State class + lines.append(f"class {state_class} extends State<{widget_class}> {{") + + # build() + lines.append(" @override") + lines.append(" Widget build(BuildContext context) {") + lines.append(" try {") + + # Read properties using typed getters + if plan.properties: + for prop in plan.properties: + dart_var = _to_camel_case(prop.python_name) + if prop.dart_getter: + # Use the pre-computed typed getter from the analyzer + if prop.dart_getter.startswith("widget.") or prop.dart_getter.startswith( + "buildWidget" + ): + getter_expr = prop.dart_getter + else: + getter_expr = prop.dart_getter.replace("control.", "widget.control.") + lines.append(f" final {dart_var} = {getter_expr};") + else: + # Fallback to getString + lines.append( + f' final {dart_var} = widget.control.getString("{prop.python_name}");' + ) + lines.append("") + + # Build SDK widget constructor args + if plan.widget_family_variants: + # Family variant: switch on type to select the right SDK widget + lines.append(" final Widget sdkChild;") + default_val = plan.widget_family_variants[0].enum_value + lines.append(f' final type = widget.control.getString("type") ?? "{default_val}";') + lines.append(" switch (type) {") + # Build shared args string (all properties except "type") + shared_args: list[str] = [] + for prop in plan.properties: + if prop.python_name == "type": + continue + dart_var = _to_camel_case(prop.python_name) + dart_param = prop.dart_name or prop.python_name + shared_args.append(f"{dart_param}: {dart_var}") + args_str = ", ".join(shared_args) + for variant in plan.widget_family_variants: + lines.append(f' case "{variant.enum_value}":') + lines.append(f" sdkChild = {variant.dart_class_name}({args_str});") + lines.append(" break;") + # Default case + default_cls = plan.widget_family_variants[0].dart_class_name + lines.append(" default:") + lines.append(f" sdkChild = {default_cls}({args_str});") + lines.append(" }") + lines.append("") + lines.append(" return LayoutControl(") + lines.append(" control: widget.control,") + lines.append(" child: sdkChild,") + lines.append(" );") + else: + lines.append(" return LayoutControl(") + lines.append(" control: widget.control,") + lines.append(f" child: {sdk_class}(") + for prop in plan.properties: + dart_var = _to_camel_case(prop.python_name) + # Use the original Dart param name for the constructor + dart_param = prop.dart_name or prop.python_name + lines.append(f" {dart_param}: {dart_var},") + lines.append(" ),") + lines.append(" );") + + # Error handling + lines.append(" } catch (error, stackTrace) {") + lines.append(" _handleError(error, stackTrace);") + lines.append(" return const SizedBox.shrink();") + lines.append(" }") + lines.append(" }") + lines.append("") + + # _handleError + lines.append(" void _handleError(Object error, StackTrace stackTrace) {") + lines.append(f' debugPrint("{widget_class} ERROR: $error");') + lines.append(' widget.control.triggerEvent("error", {') + lines.append(' "message": error.toString(),') + lines.append(' "stack_trace": stackTrace.toString(),') + lines.append(" });") + lines.append(" }") + + lines.append("}") + lines.append("") + + # Sub-control StatefulWidgets + for sub in self._flatten_sub_controls(plan.sub_controls): + lines.extend(self._render_sub_control_widget(sub, plan)) + lines.append("") + + return "\n".join(lines) + + # ------------------------------------------------------------------ + # Sibling widget generation + # ------------------------------------------------------------------ + + def _generate_sibling_widget(self, sibling: SiblingWidgetPlan, plan: GenerationPlan) -> str: + """Generate a standalone StatefulWidget Dart file for a sibling widget.""" + widget_class = f"{sibling.control_name}Widget" + state_class = f"_{sibling.control_name}WidgetState" + sdk_class = sibling.dart_class_name + needs_material = any( + "Theme.of(context)" in (p.dart_getter or "") for p in sibling.properties + ) + flutter_import = ( + "import 'package:flutter/material.dart';" + if needs_material + else "import 'package:flutter/widgets.dart';" + ) + lines: list[str] = [ + "import 'package:flet/flet.dart';", + flutter_import, + ] + + # Imports + if plan.dart_import: + lines.append(f"import '{plan.dart_import}';") + lines.append("") + + # StatefulWidget + lines.append(f"/// {sibling.control_name} widget implementation for Flet.") + lines.append(f"class {widget_class} extends StatefulWidget {{") + lines.append(" final Control control;") + lines.append(f" const {widget_class}({{super.key, required this.control}});") + lines.append("") + lines.append(" @override") + lines.append(f" State<{widget_class}> createState() => {state_class}();") + lines.append("}") + lines.append("") + + # State class + lines.append(f"class {state_class} extends State<{widget_class}> {{") + lines.append(" @override") + lines.append(" Widget build(BuildContext context) {") + lines.append(" try {") + + # Read properties + if sibling.properties: + for prop in sibling.properties: + dart_var = _to_camel_case(prop.python_name) + if prop.dart_getter: + getter_expr = prop.dart_getter.replace("control.", "widget.control.") + if getter_expr.startswith("buildWidget"): + getter_expr = prop.dart_getter + lines.append(f" final {dart_var} = {getter_expr};") + else: + lines.append( + f' final {dart_var} = widget.control.getString("{prop.python_name}");' + ) + lines.append("") + + # SDK widget constructor + lines.append(" return LayoutControl(") + lines.append(" control: widget.control,") + lines.append(f" child: {sdk_class}(") + for prop in sibling.properties: + dart_var = _to_camel_case(prop.python_name) + dart_param = prop.dart_name or prop.python_name + lines.append(f" {dart_param}: {dart_var},") + lines.append(" ),") + lines.append(" );") + + # Error handling + lines.append(" } catch (error, stackTrace) {") + lines.append(" _handleError(error, stackTrace);") + lines.append(" return const SizedBox.shrink();") + lines.append(" }") + lines.append(" }") + lines.append("") + + # _handleError + lines.append(" void _handleError(Object error, StackTrace stackTrace) {") + lines.append(f' debugPrint("{widget_class} ERROR: $error");') + lines.append(' widget.control.triggerEvent("error", {') + lines.append(' "message": error.toString(),') + lines.append(' "stack_trace": stackTrace.toString(),') + lines.append(" });") + lines.append(" }") + + lines.append("}") + lines.append("") + + return "\n".join(lines) + + def _generate_extension_dart(self, plan: GenerationPlan) -> str: + """Generate extension.dart that registers main + sibling widgets.""" + control_snake = plan.control_name_snake or camel_to_snake(plan.control_name) + lines: list[str] = [] + + # Imports + lines.append("import 'package:flet/flet.dart';") + lines.append("import 'package:flutter/widgets.dart';") + lines.append(f"import 'src/{control_snake}_widget.dart';") + for sibling in plan.sibling_widgets: + sib_snake = sibling.control_name_snake or camel_to_snake(sibling.control_name) + lines.append(f"import 'src/{sib_snake}_widget.dart';") + lines.append("") + + # createControl function + lines.append("Widget? createControl(Control control) {") + lines.append(" switch (control.type) {") + lines.append(f' case "{plan.control_name}":') + lines.append(f" return {plan.control_name}Widget(control: control);") + for sibling in plan.sibling_widgets: + lines.append(f' case "{sibling.control_name}":') + lines.append(f" return {sibling.control_name}Widget(control: control);") + lines.append(" default:") + lines.append(" return null;") + lines.append(" }") + lines.append("}") + lines.append("") + + return "\n".join(lines) + + # ------------------------------------------------------------------ + # Sub-control widget rendering + # ------------------------------------------------------------------ + + def _render_sub_control_widget(self, sub: SubControlPlan, plan: GenerationPlan) -> list[str]: + """Render a StatefulWidget for a sub-control.""" + widget_class = f"{sub.control_name}Widget" + state_class = f"_{sub.control_name}WidgetState" + lines: list[str] = [] + + lines.append(f"/// {sub.control_name} sub-control widget for {plan.control_name}.") + lines.append(f"class {widget_class} extends StatefulWidget {{") + lines.append(" final Control control;") + lines.append(f" const {widget_class}({{super.key, required this.control}});") + lines.append("") + lines.append(" @override") + lines.append(f" State<{widget_class}> createState() => {state_class}();") + lines.append("}") + lines.append("") + + lines.append(f"class {state_class} extends State<{widget_class}> {{") + lines.append(" @override") + lines.append(" Widget build(BuildContext context) {") + lines.append(" try {") + + # Read properties using typed getters + for prop in sub.properties: + dart_var = _to_camel_case(prop.python_name) + if prop.dart_getter: + if prop.dart_getter.startswith("widget.") or prop.dart_getter.startswith( + "buildWidget" + ): + getter_expr = prop.dart_getter + else: + getter_expr = prop.dart_getter.replace("control.", "widget.control.") + lines.append(f" final {dart_var} = {getter_expr};") + else: + lines.append( + f' final {dart_var} = widget.control.getString("{prop.python_name}");' + ) + + if sub.properties: + lines.append("") + + # Build constructor call + lines.append(f" return {sub.dart_class_name}(") + for prop in sub.properties: + dart_var = _to_camel_case(prop.python_name) + dart_param = prop.dart_name or prop.python_name + lines.append(f" {dart_param}: {dart_var},") + lines.append(" );") + lines.append(" } catch (error, stackTrace) {") + lines.append(f' debugPrint("{sub.dart_class_name} ERROR: $error");') + lines.append(" return const SizedBox.shrink();") + lines.append(" }") + lines.append(" }") + lines.append("}") + lines.append("") + + return lines + + @staticmethod + def _flatten_sub_controls(sub_controls: list[SubControlPlan]) -> list[SubControlPlan]: + """Flatten a recursive sub-control tree (leaves first, deduplicated).""" + result: list[SubControlPlan] = [] + seen: set[str] = set() + for sc in sub_controls: + for nested in DartServiceGenerator._flatten_sub_controls(sc.sub_controls): + if nested.control_name not in seen: + result.append(nested) + seen.add(nested.control_name) + if sc.control_name not in seen: + result.append(sc) + seen.add(sc.control_name) + return result + + # ------------------------------------------------------------------ + # Initialize method + # ------------------------------------------------------------------ + + def _render_initialize(self, lines: list[str], plan: GenerationPlan, class_name: str) -> None: + main_class = plan.dart_main_class or plan.control_name + + lines.append(f" /// Initialize the {plan.control_name} SDK.") + lines.append(f" void _initialize{plan.control_name}() {{") + lines.append(" if (_initialized) return;") + lines.append("") + lines.append(" try {") + + if plan.properties: + # Read properties from control + for prop in plan.properties: + dart_var = _to_camel_case(prop.python_name) + if prop.python_type in ("str", "str | None"): + lines.append( + f' final {dart_var} = control.getString("{prop.python_name}");' + ) + elif prop.python_type == "bool": + lines.append( + f' final {dart_var} = control.getBool("{prop.python_name}", false)!;' + ) + elif prop.python_type in ("int", "int | None"): + lines.append(f' final {dart_var} = control.getInt("{prop.python_name}");') + else: + lines.append( + f' final {dart_var} = control.getString("{prop.python_name}");' + ) + lines.append("") + + # Apply pre-init setter properties BEFORE initialize() + pre_init_props = [p for p in plan.properties if p.dart_pre_init_call] + if pre_init_props: + lines.append(" // Pre-init configuration") + for prop in pre_init_props: + dart_var = _to_camel_case(prop.python_name) + call = prop.dart_pre_init_call.replace("{var}", dart_var) + lines.append(f" {call}") + lines.append("") + + # Find the init property (app_id, api_key, id, key, or first string property) + init_prop = None + for prop in plan.properties: + if prop.python_name in ("app_id", "api_key", "id", "key"): + init_prop = prop + break + if not init_prop: + # First string property that is NOT a pre-init setter + for prop in plan.properties: + if prop.python_type in ("str", "str | None") and not prop.dart_pre_init_call: + init_prop = prop + break + + if init_prop: + var = _to_camel_case(init_prop.python_name) + lines.append(f" if ({var} != null && {var}.isNotEmpty) {{") + lines.append(f" {main_class}.initialize({var});") + lines.append(" }") + else: + lines.append(f" // Initialize {plan.control_name} SDK") + lines.append(f" // {main_class}.initialize(...);") + + lines.append("") + lines.append(" _initialized = true;") + lines.append(" _setupListeners();") + lines.append(" } catch (error, stackTrace) {") + lines.append(f' _handleError("_initialize{plan.control_name}", error, stackTrace);') + lines.append(" }") + lines.append(" }") + lines.append("") + + # ------------------------------------------------------------------ + # Setup listeners with real event registrations + # ------------------------------------------------------------------ + + def _render_setup_listeners( + self, lines: list[str], plan: GenerationPlan, class_name: str + ) -> None: + lines.append(" /// Setup all event listeners.") + lines.append(" void _setupListeners() {") + lines.append(" if (_listenersSetup) return;") + lines.append(" _listenersSetup = true;") + + if plan.events: + lines.append("") + for event in plan.events: + accessor = event.dart_sdk_accessor or plan.dart_main_class + listener_method = event.dart_listener_method + event_name = event.dart_event_name + + if not listener_method: + continue + + # Generate real listener registration + lines.append(f" // {event.python_attr_name}") + lines.append(f" {accessor}.{listener_method}((event) {{") + lines.append(" try {") + lines.append(f' control.triggerEvent("{event_name}", {{') + + # Generate event data extraction based on fields + for field_name, field_type in event.fields: + camel = _to_camel_case(field_name) + if field_name == "data": + lines.append(f' "{field_name}": event.toString(),') + elif field_name == "value": + # Simple value callback (e.g., bool for permission) + lines.append(f' "{field_name}": event,') + elif field_type == "dict": + # Complex object → serialize via jsonRepresentation + lines.append( + f' "{field_name}": ' + f"jsonDecode(event.{camel}.jsonRepresentation())," + ) + elif field_type in ("str", "str | None"): + lines.append(f' "{field_name}": event.{camel},') + elif field_type in ("bool", "bool | None", "int", "int | None", "float"): + lines.append(f' "{field_name}": event.{camel},') + else: + lines.append(f' "{field_name}": event.{camel}?.toString(),') + + lines.append(" });") + lines.append(" } catch (error, stackTrace) {") + lines.append(f' _handleError("{event_name}_listener", error, stackTrace);') + lines.append(" }") + lines.append(" });") + lines.append("") + + lines.append(f' debugPrint("{class_name}._setupListeners: ready");') + lines.append(" }") + lines.append("") + + # ------------------------------------------------------------------ + # Enum parser helpers + # ------------------------------------------------------------------ + + def _render_enum_helpers(self, lines: list[str], plan: GenerationPlan) -> None: + """Render helper methods to parse enum string values.""" + for enum in plan.enums: + dart_name = enum.python_name + lines.append(f" {dart_name} _parse{dart_name}(String value) {{") + lines.append(" return switch (value.toLowerCase()) {") + for val_name, val_value, _val_doc in enum.values: + lines.append(f' "{val_value}" => {dart_name}.{val_name},') + # Default fallback + if enum.values: + default_val = enum.values[0][0] + lines.append(f" _ => {dart_name}.{default_val},") + lines.append(" };") + lines.append(" }") + lines.append("") + + # ------------------------------------------------------------------ + # _onInvokeMethod dispatch + # ------------------------------------------------------------------ + + def _render_invoke_method(self, lines: list[str], plan: GenerationPlan) -> None: + lines.append(" /// Handle method invocations from Python.") + lines.append(" Future _onInvokeMethod(String methodName, dynamic args) async {") + lines.append(" try {") + lines.append(" Map arguments = {};") + lines.append(" if (args != null && args is Map) {") + lines.append(" arguments = Map.from(args);") + lines.append(" }") + lines.append("") + lines.append(" return switch (methodName) {") + + # Main methods + if plan.main_methods: + lines.append(" // Main methods") + for method in plan.main_methods: + dart_name = method.dart_method_name + impl_name = _to_dart_method_name(dart_name) + if method.params: + lines.append(f' "{dart_name}" => await {impl_name}(arguments),') + else: + lines.append(f' "{dart_name}" => await {impl_name}(),') + + # Sub-module methods + for sub in plan.sub_modules: + label = sub.module_name.replace("_", " ").title() + lines.append(f" // {label} methods") + for method in sub.methods: + dart_name = method.dart_method_name + impl_name = _to_dart_method_name(dart_name) + if method.params: + lines.append(f' "{dart_name}" => await {impl_name}(arguments),') + else: + lines.append(f' "{dart_name}" => await {impl_name}(),') + + lines.append( + f' _ => throw Exception("Unknown {plan.control_name} method: $methodName"),' + ) + lines.append(" };") + lines.append(" } catch (error, stackTrace) {") + lines.append(" _handleError(methodName, error, stackTrace);") + lines.append(" return null;") + lines.append(" }") + lines.append(" }") + lines.append("") + + # ------------------------------------------------------------------ + # Individual method implementations with real SDK calls + # ------------------------------------------------------------------ + + def _render_dart_method( + self, + method: MethodPlan, + plan: GenerationPlan, + sub_module: SubModulePlan | None, + ) -> list[str]: + """Render a single Dart method with a real SDK call.""" + lines: list[str] = [] + impl_name = _to_dart_method_name(method.dart_method_name) + + # Determine SDK call path + if sub_module and sub_module.dart_sdk_accessor: + sdk_accessor = sub_module.dart_sdk_accessor + else: + sdk_accessor = plan.dart_main_class or plan.control_name + + # Original Dart method name for the SDK call + original_name = method.dart_original_name or method.python_name + dart_return = method.return_type + + if method.params: + lines.append(f" Future {impl_name}(Map args) async {{") + # Extract arguments using python_name as key (matches what Python sends) + for p in method.params: + dart_type = _dart_cast_type(p.dart_type) + var_name = _safe_dart_var(p.dart_name) + lines.append(f' final {var_name} = args["{p.python_name}"] as {dart_type};') + + # Build null check condition for required params + required_params = [p for p in method.params if not p.is_optional] + + # Build SDK call arguments (use "name: name" for named params) + # Required params are passed directly; optional params use + # conditional forwarding so they are only sent when non-null. + sdk_arg_parts = [] + optional_parts = [] + for p in method.params: + v = _safe_dart_var(p.dart_name) + if p.is_optional: + if p.is_named: + optional_parts.append((p.dart_name, v)) + else: + if p.is_named: + sdk_arg_parts.append(f"{p.dart_name}: {v}") + else: + sdk_arg_parts.append(v) + # Add optional named params with null check + for orig_name, safe_name in optional_parts: + sdk_arg_parts.append(f"if ({safe_name} != null) {orig_name}: {safe_name}") + sdk_args = ", ".join(sdk_arg_parts) + + if required_params: + checks = " && ".join( + f"{_safe_dart_var(p.dart_name)} != null" for p in required_params + ) + lines.append(f" if ({checks}) {{") + + # Determine return handling + if dart_return == "None": + lines.append(f" await {sdk_accessor}.{original_name}({sdk_args});") + lines.append(" }") + lines.append(" return null;") + elif "bool" in dart_return.lower(): + lines.append( + f" final result = await {sdk_accessor}.{original_name}({sdk_args});" + ) + lines.append(" return result.toString();") + lines.append(" }") + lines.append(" return null;") + else: + lines.append( + f" final result = await {sdk_accessor}.{original_name}({sdk_args});" + ) + lines.append(" return result?.toString();") + lines.append(" }") + lines.append(" return null;") + else: + # All params are optional - call directly + if dart_return == "None": + lines.append(f" await {sdk_accessor}.{original_name}({sdk_args});") + lines.append(" return null;") + else: + lines.append( + f" final result = await {sdk_accessor}.{original_name}({sdk_args});" + ) + lines.append(" return result?.toString();") + else: + # No params + lines.append(f" Future {impl_name}() async {{") + + # Getters use property access (no parens), methods use call syntax + call = ( + f"{sdk_accessor}.{original_name}" + if method.is_getter + else f"{sdk_accessor}.{original_name}()" + ) + + if dart_return == "None": + lines.append(f" await {call};") + lines.append(" return null;") + elif "bool" in dart_return.lower(): + lines.append(f" final result = await {call};") + lines.append(" return result.toString();") + elif dart_return in ("str", "str | None"): + lines.append(f" return await {call};") + elif dart_return.startswith("dict") or dart_return.startswith("list"): + lines.append(f" final result = await {call};") + lines.append(" return jsonEncode(result);") + else: + lines.append(f" final result = await {call};") + lines.append(" return result?.toString();") + + lines.append(" }") + return lines + + # ------------------------------------------------------------------ + # Error handler + # ------------------------------------------------------------------ + + def _render_handle_error(self, lines: list[str], plan: GenerationPlan, class_name: str) -> None: + lines.append(" /// Handle and report errors.") + lines.append(" void _handleError(String method, Object error, StackTrace stackTrace) {") + lines.append(f' debugPrint("{class_name} ERROR in $method: $error");') + lines.append(" FlutterError.reportError(FlutterErrorDetails(") + lines.append(" exception: error,") + lines.append(" stack: stackTrace,") + lines.append(f" library: '{plan.package_name}',") + ctx_desc = f"while executing {plan.control_name} method" + lines.append(f" context: ErrorDescription('{ctx_desc} \"$method\"'),") + lines.append(" ));") + lines.append(' control.triggerEvent("error", {') + lines.append(' "method": method,') + lines.append(' "message": error.toString(),') + lines.append(' "stack_trace": stackTrace.toString(),') + lines.append(" });") + lines.append(" }") + lines.append("") + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + def _find_sub_module_for_method( + self, method: MethodPlan, plan: GenerationPlan + ) -> SubModulePlan | None: + """Find the sub-module that contains a method.""" + for sub in plan.sub_modules: + if method in sub.methods: + return sub + return None + + +def _to_dart_method_name(snake_name: str) -> str: + """Convert a snake_case dart method name to a _camelCase Dart function name.""" + parts = snake_name.split("_") + return "_" + parts[0] + "".join(p.capitalize() for p in parts[1:]) + + +def _to_camel_case(snake_name: str) -> str: + """Convert snake_case to camelCase.""" + parts = snake_name.split("_") + return parts[0] + "".join(p.capitalize() for p in parts[1:]) + + +_DART_RESERVED = frozenset( + { + "this", + "super", + "class", + "enum", + "extends", + "with", + "implements", + "abstract", + "as", + "assert", + "break", + "case", + "catch", + "const", + "continue", + "default", + "do", + "else", + "false", + "final", + "finally", + "for", + "if", + "in", + "is", + "new", + "null", + "return", + "switch", + "throw", + "true", + "try", + "var", + "void", + "while", + "yield", + } +) + + +def _safe_dart_var(name: str) -> str: + """Escape Dart reserved keywords used as variable names.""" + if name in _DART_RESERVED: + return f"{name}_" + return name + + +def _dart_cast_type(dart_type: str) -> str: + """Return a safe Dart cast type for argument extraction.""" + dart_type = dart_type.strip().rstrip("?") + if dart_type in ("String", "bool", "int", "double"): + return f"{dart_type}?" + if dart_type.startswith("Map"): + return "Map?" + if dart_type.startswith("List"): + return "List?" + return "dynamic" diff --git a/src/flet_pkg/core/generators/python_control.py b/src/flet_pkg/core/generators/python_control.py new file mode 100644 index 0000000..e4d17e6 --- /dev/null +++ b/src/flet_pkg/core/generators/python_control.py @@ -0,0 +1,483 @@ +"""Generator for the main Python control file. + +Produces the primary control class (e.g., ``onesignal.py``) following +the Flet 0.80.x+ extension pattern with ``@ft.control()``, sub-module +properties, event handlers, and async methods. +""" + +from flet_pkg.core.generators.base import CodeGenerator +from flet_pkg.core.models import GenerationPlan, MethodPlan, SiblingWidgetPlan, SubControlPlan +from flet_pkg.core.parser import camel_to_snake + + +class PythonControlGenerator(CodeGenerator): + """Generates the main Python control file.""" + + def generate(self, plan: GenerationPlan) -> dict[str, str]: + """Generate the main Python control file. + + Args: + plan: Generation plan produced by the analyzer. + + Returns: + Mapping of filename to generated source code. + """ + control_snake = plan.control_name_snake or camel_to_snake(plan.control_name) + filename = f"{control_snake}.py" + lines: list[str] = [] + + # Module docstring + kind = "Service" if plan.base_class == "ft.Service" else "Control" + lines.append('"""') + lines.append(f"{plan.control_name} {kind} for {plan.package_name}.") + if plan.description: + lines.append(f"\n{plan.description}") + lines.append('"""') + + # Imports + is_service = plan.base_class == "ft.Service" + lines.append("") + all_props = list(plan.properties) + for sc in self._flatten_sub_controls(plan.sub_controls): + all_props.extend(sc.properties) + needs_field = bool(plan.sub_modules) or any( + p.default_value.startswith("field(") for p in all_props + ) + if needs_field: + lines.append("from dataclasses import field") + + # Only import Any when needed (service _invoke_method, method signatures, + # properties, or sub-control properties) + needs_any = ( + is_service + or any("Any" in p.python_type for m in plan.main_methods for p in m.params) + or any("Any" in m.return_type for m in plan.main_methods) + or any("Any" in p.python_type for p in plan.properties) + or any("Any" in p.python_type for sc in plan.sub_controls for p in sc.properties) + ) + if needs_any: + lines.append("from typing import Any, Optional") + else: + lines.append("from typing import Optional") + lines.append("") + lines.append("import flet as ft") + lines.append("") + + # Sub-module imports + for sub in plan.sub_modules: + lines.append(f"from {plan.package_name}.{sub.module_name} import {sub.class_name}") + + # Type imports — only import types actually used by properties/events + type_imports: list[str] = [] + for event in plan.events: + type_imports.append(event.event_class_name) + # Sub-control event types + for sub in self._flatten_sub_controls(plan.sub_controls): + for event in sub.events: + type_imports.append(event.event_class_name) + # Add error event + type_imports.append(plan.error_event_class or f"{plan.control_name}ErrorEvent") + + # Collect type names referenced by properties AND method signatures + used_type_names: set[str] = set() + + # Gather all type strings to scan: properties + method params + returns + type_strings: list[str] = [p.python_type for p in plan.properties] + for method in plan.main_methods: + type_strings.append(method.return_type) + for p in method.params: + type_strings.append(p.python_type) + for sub in plan.sub_modules: + for method in sub.methods: + type_strings.append(method.return_type) + for p in method.params: + type_strings.append(p.python_type) + + for type_str in type_strings: + for enum in plan.enums: + if enum.python_name in type_str: + used_type_names.add(enum.python_name) + for stub in plan.stub_data_classes: + if stub.python_name in type_str: + used_type_names.add(stub.python_name) + + for enum in plan.enums: + if enum.python_name in used_type_names: + type_imports.append(enum.python_name) + for stub in plan.stub_data_classes: + if stub.python_name in used_type_names: + type_imports.append(stub.python_name) + if type_imports: + lines.append(f"from {plan.package_name}.types import (") + for imp in sorted(set(type_imports)): + lines.append(f" {imp},") + lines.append(")") + + lines.append("") + lines.append("") + + # Sub-control classes (emitted before the main class for forward refs) + for sub in self._flatten_sub_controls(plan.sub_controls): + lines.extend(self._render_sub_control(sub)) + lines.append("") + lines.append("") + + # Class definition + lines.append(f'@ft.control("{plan.control_name}")') + lines.append(f"class {plan.control_name}({plan.base_class}):") + lines.append(' """') + lines.append(f" {plan.control_name} integration for Flet applications.") + lines.append("") + lines.append(" Example:") + lines.append(" ```python") + lines.append(" import flet as ft") + lines.append(f" import {plan.package_name} as pkg") + lines.append("") + lines.append(" async def main(page: ft.Page):") + + if plan.base_class == "ft.Service": + if plan.properties: + prop_examples = [] + for prop in plan.properties[:2]: + if prop.python_type in ("str", "str | None"): + prop_examples.append(f'{prop.python_name}="..."') + elif prop.python_type == "bool": + prop_examples.append(f"{prop.python_name}=True") + props_str = ", ".join(prop_examples) if prop_examples else "" + lines.append(f" svc = pkg.{plan.control_name}({props_str})") + else: + lines.append(f" svc = pkg.{plan.control_name}()") + lines.append(" page.services.append(svc)") + else: + lines.append(f" widget = pkg.{plan.control_name}()") + lines.append(" page.add(widget)") + + lines.append("") + lines.append(" ft.run(main)") + lines.append(" ```") + lines.append(' """') + lines.append("") + + # Properties (dataclass fields sent to Flutter) + for prop in plan.properties: + default = prop.default_value + lines.append(f" {prop.python_name}: {prop.python_type} = {default}") + if prop.docstring: + lines.append(f' """{prop.docstring}"""') + lines.append("") + + # Event handlers + for event in plan.events: + attr = event.python_attr_name + evt_cls = event.event_class_name + lines.append(f" {attr}: Optional[ft.EventHandler[{evt_cls}]] = None") + desc = event.dart_event_name.replace("_", " ") + lines.append(f' """Called when {desc} occurs."""') + lines.append("") + + # Error event handler + error_cls = plan.error_event_class or f"{plan.control_name}ErrorEvent" + lines.append(f" on_error: Optional[ft.EventHandler[{error_cls}]] = None") + lines.append(' """Called when an error occurs in the SDK."""') + lines.append("") + + # Sub-module private fields + for sub in plan.sub_modules: + lines.append( + f" _{sub.module_name}: {sub.class_name} = field(" + f'default=None, init=False, metadata={{"skip": True}})' + ) + if plan.sub_modules: + lines.append("") + + # init() method + if plan.sub_modules: + lines.append(" def init(self):") + lines.append(' """Initialize the service and sub-modules."""') + lines.append(" super().init()") + for sub in plan.sub_modules: + lines.append(f" self._{sub.module_name} = {sub.class_name}(self)") + lines.append("") + + # Sub-module properties + for sub in plan.sub_modules: + lines.append(" @property") + lines.append(f" def {sub.module_name}(self) -> {sub.class_name}:") + mod_title = sub.module_name.replace("_", " ").title() + lines.append(f' """Access the {mod_title} namespace."""') + lines.append(f" if self._{sub.module_name} is None:") + lines.append(f" self._{sub.module_name} = {sub.class_name}(self)") + lines.append(f" return self._{sub.module_name}") + lines.append("") + + # Main methods + for method in plan.main_methods: + lines.extend(self._render_method(method, plan)) + lines.append("") + + # Platform guard and invoke_method only for service extensions + # (UI controls are pure Dart widgets that work on all platforms) + is_service = plan.base_class == "ft.Service" + if is_service: + lines.append(" def _is_supported_platform(self) -> bool:") + lines.append( + f' """Check if the current platform supports {plan.control_name}."""' + ) + lines.append(" if not self.page:") + lines.append(" return False") + lines.append(" return self.page.platform in (") + lines.append(" ft.PagePlatform.ANDROID,") + lines.append(" ft.PagePlatform.IOS,") + lines.append(" ft.PagePlatform.MACOS,") + lines.append(" ft.PagePlatform.WINDOWS,") + lines.append(" ft.PagePlatform.LINUX,") + lines.append(" )") + lines.append("") + + lines.append(" async def _invoke_method(") + lines.append(" self,") + lines.append(" method_name: str,") + lines.append(" arguments: Optional[dict[str, Any]] = None,") + lines.append(" timeout: Optional[float] = None,") + lines.append(" ) -> Any:") + lines.append(' """Internal method for invoking Flutter methods."""') + lines.append(" if not self._is_supported_platform():") + lines.append( + " platform_name = self.page.platform.value if self.page else 'unknown'" + ) + lines.append(" raise ft.FletUnsupportedPlatformException(") + lines.append( + f' f"{plan.control_name} is only supported on Android and iOS. "' + ) + lines.append(' f"Current platform: {platform_name}. "') + lines.append(" f\"Method '{method_name}' cannot be executed.\"") + lines.append(" )") + lines.append(" effective_timeout = timeout if timeout is not None else 25.0") + lines.append(" return await super()._invoke_method(") + lines.append(" method_name=method_name,") + lines.append(" arguments=arguments or {},") + lines.append(" timeout=effective_timeout,") + lines.append(" )") + lines.append("") + + files = {filename: "\n".join(lines)} + + # Generate sibling widget Python files + for sibling in plan.sibling_widgets: + sib_snake = sibling.control_name_snake or camel_to_snake(sibling.control_name) + sib_file = f"{sib_snake}.py" + files[sib_file] = self._generate_sibling(sibling, plan) + + return files + + def _generate_sibling(self, sibling: SiblingWidgetPlan, plan: GenerationPlan) -> str: + """Generate a Python control file for a sibling widget.""" + lines: list[str] = [] + + # Module docstring + lines.append('"""') + lines.append(f"{sibling.control_name} Control for {plan.package_name}.") + lines.append('"""') + lines.append("") + + # Check if we need field import + needs_field = any(p.default_value.startswith("field(") for p in sibling.properties) + if needs_field: + lines.append("from dataclasses import field") + needs_any = any("Any" in p.python_type for p in sibling.properties) + if needs_any: + lines.append("from typing import Any, Optional") + else: + lines.append("from typing import Optional") + lines.append("") + lines.append("import flet as ft") + lines.append("") + + # Type imports + type_imports: list[str] = [] + for event in sibling.events: + type_imports.append(event.event_class_name) + error_cls = plan.error_event_class or f"{plan.control_name}ErrorEvent" + type_imports.append(error_cls) + + # Import enums referenced by property types + for prop in sibling.properties: + for enum in plan.enums: + if enum.python_name in prop.python_type: + type_imports.append(enum.python_name) + + if type_imports: + lines.append(f"from {plan.package_name}.types import (") + for imp in sorted(set(type_imports)): + lines.append(f" {imp},") + lines.append(")") + lines.append("") + lines.append("") + + # Class definition + lines.append(f'@ft.control("{sibling.control_name}")') + lines.append(f"class {sibling.control_name}(ft.LayoutControl):") + lines.append(f' """{sibling.control_name} widget for Flet."""') + lines.append("") + + # Properties + for prop in sibling.properties: + lines.append(f" {prop.python_name}: {prop.python_type} = {prop.default_value}") + lines.append("") + + # Events + for event in sibling.events: + attr = event.python_attr_name + evt_cls = event.event_class_name + lines.append(f" {attr}: Optional[ft.EventHandler[{evt_cls}]] = None") + lines.append("") + + # Error event handler + lines.append(f" on_error: Optional[ft.EventHandler[{error_cls}]] = None") + lines.append(' """Called when an error occurs."""') + lines.append("") + + return "\n".join(lines) + + def _render_method(self, method: MethodPlan, plan: GenerationPlan) -> list[str]: + """Render a single async method.""" + lines: list[str] = [] + + # Build signature + sig_parts = ["self"] + for p in method.params: + type_hint = p.python_type + if p.is_optional: + if "None" not in type_hint: + type_hint = f"{type_hint} | None" + default = self._py_default(p.default) + sig_parts.append(f"{p.python_name}: {type_hint} = {default}") + else: + sig_parts.append(f"{p.python_name}: {type_hint}") + + # All methods are async because they call _invoke_method + sig = ", ".join(sig_parts) + lines.append(f" async def {method.python_name}({sig}) -> {method.return_type}:") + + # Docstring + if method.docstring: + lines.append(' """') + for dl in method.docstring.split("\n"): + lines.append(f" {dl}" if dl.strip() else "") + if method.params: + lines.append("") + lines.append(" Args:") + for p in method.params: + if p.docstring: + lines.append(f" {p.python_name}: {p.docstring}") + else: + lines.append(f" {p.python_name}: {p.dart_name} parameter.") + lines.append(' """') + elif method.params: + doc = method.python_name.replace("_", " ").capitalize() + lines.append(f' """{doc}.') + lines.append("") + lines.append(" Args:") + for p in method.params: + if p.docstring: + lines.append(f" {p.python_name}: {p.docstring}") + else: + lines.append(f" {p.python_name}: {p.dart_name} parameter.") + lines.append(' """') + else: + doc = method.python_name.replace("_", " ").capitalize() + lines.append(f' """{doc}."""') + + # Build arguments dict using python_name as key (matches Dart side) + # For enum-typed params, use .value to serialize (with None guard) + enum_names = {e.python_name for e in plan.enums} + args_dict: dict[str, str] = {} + for p in method.params: + base_type = p.python_type.replace("Optional[", "").rstrip("]") + base_type = base_type.split("|")[0].strip() + if base_type in enum_names: + if p.is_optional or "None" in p.python_type: + args_dict[p.python_name] = ( + f"{p.python_name}.value if {p.python_name} is not None else None" + ) + else: + args_dict[p.python_name] = f"{p.python_name}.value" + else: + args_dict[p.python_name] = p.python_name + + if args_dict: + args_str = ", ".join(f'"{k}": {v}' for k, v in args_dict.items()) + invoke_args = f"{{{args_str}}}" + else: + invoke_args = "" + + # Method body + if method.return_type == "None": + if invoke_args: + lines.append( + f' await self._invoke_method("{method.dart_method_name}", {invoke_args})' + ) + else: + lines.append(f' await self._invoke_method("{method.dart_method_name}")') + elif "bool" in method.return_type.lower(): + if invoke_args: + lines.append( + f" result = await self._invoke_method(" + f'"{method.dart_method_name}", {invoke_args})' + ) + else: + lines.append( + f' result = await self._invoke_method("{method.dart_method_name}")' + ) + lines.append(' return result == "true"') + else: + if invoke_args: + lines.append( + f" return await self._invoke_method(" + f'"{method.dart_method_name}", {invoke_args})' + ) + else: + lines.append( + f' return await self._invoke_method("{method.dart_method_name}")' + ) + + return lines + + @staticmethod + def _flatten_sub_controls(sub_controls: list[SubControlPlan]) -> list[SubControlPlan]: + """Flatten a recursive sub-control tree (leaves first, deduplicated).""" + result: list[SubControlPlan] = [] + seen: set[str] = set() + for sc in sub_controls: + for nested in PythonControlGenerator._flatten_sub_controls(sc.sub_controls): + if nested.control_name not in seen: + result.append(nested) + seen.add(nested.control_name) + if sc.control_name not in seen: + result.append(sc) + seen.add(sc.control_name) + return result + + def _render_sub_control(self, sub: SubControlPlan) -> list[str]: + """Render a sub-control class definition.""" + lines: list[str] = [] + lines.append(f'@ft.control("{sub.control_name}")') + lines.append(f"class {sub.control_name}(ft.Control):") + lines.append(f' """{sub.control_name} sub-control."""') + lines.append("") + + for prop in sub.properties: + lines.append(f" {prop.python_name}: {prop.python_type} = {prop.default_value}") + lines.append("") + + for event in sub.events: + attr = event.python_attr_name + evt_cls = event.event_class_name + lines.append(f" {attr}: Optional[ft.EventHandler[{evt_cls}]] = None") + lines.append("") + + # If no properties or events, add pass + if not sub.properties and not sub.events: + lines.append(" pass") + + return lines diff --git a/src/flet_pkg/core/generators/python_init.py b/src/flet_pkg/core/generators/python_init.py new file mode 100644 index 0000000..ce3cb54 --- /dev/null +++ b/src/flet_pkg/core/generators/python_init.py @@ -0,0 +1,164 @@ +"""Generator for the Python __init__.py file. + +Produces the package's ``__init__.py`` with all public exports +and ``__all__`` definition. +""" + +from flet_pkg.core.generators.base import CodeGenerator +from flet_pkg.core.models import GenerationPlan, SubControlPlan +from flet_pkg.core.parser import camel_to_snake + + +class PythonInitGenerator(CodeGenerator): + """Generates __init__.py with exports and __all__.""" + + def generate(self, plan: GenerationPlan) -> dict[str, str]: + """Generate ``__init__.py`` with public exports and ``__all__``. + + Args: + plan: Generation plan produced by the analyzer. + + Returns: + Mapping of filename to generated source code. + """ + lines: list[str] = [] + all_exports: list[str] = [] + + control_snake = plan.control_name_snake or camel_to_snake(plan.control_name) + + # Module docstring + desc = plan.description or f"{plan.control_name} integration for Flet applications." + lines.append('"""') + lines.append(f"{plan.package_name} - {desc}") + lines.append('"""') + lines.append("") + + # Main control import + lines.append(f"from {plan.package_name}.{control_snake} import {plan.control_name}") + all_exports.append(plan.control_name) + + # Sub-control imports (from the same control module) + for sub in self._flatten_sub_controls(plan.sub_controls): + lines.append(f"from {plan.package_name}.{control_snake} import {sub.control_name}") + all_exports.append(sub.control_name) + + # Sibling widget imports (from separate modules) + for sibling in plan.sibling_widgets: + sib_snake = sibling.control_name_snake or camel_to_snake(sibling.control_name) + lines.append(f"from {plan.package_name}.{sib_snake} import {sibling.control_name}") + all_exports.append(sibling.control_name) + + # Sub-module imports + for sub in plan.sub_modules: + lines.append(f"from {plan.package_name}.{sub.module_name} import {sub.class_name}") + all_exports.append(sub.class_name) + + # Types imports (always include error event class) + type_names: list[str] = [] + for enum in plan.enums: + type_names.append(enum.python_name) + for event in plan.events: + type_names.append(event.event_class_name) + for stub in plan.stub_data_classes: + type_names.append(stub.python_name) + # Sub-control event types + for sub in self._flatten_sub_controls(plan.sub_controls): + for event in sub.events: + type_names.append(event.event_class_name) + # Sibling event types + for sibling in plan.sibling_widgets: + for event in sibling.events: + type_names.append(event.event_class_name) + # Error event is always generated + type_names.append(plan.error_event_class or f"{plan.control_name}ErrorEvent") + type_names = sorted(set(type_names)) + + lines.append(f"from {plan.package_name}.types import (") + for name in type_names: + lines.append(f" {name},") + lines.append(")") + all_exports.extend(type_names) + + # Console imports (conditional) + if plan.include_console: + lines.append(f"from {plan.package_name}.console import DebugConsole, setup_logging") + all_exports.extend(["DebugConsole", "setup_logging"]) + + lines.append("") + + # __all__ + lines.append("__all__ = [") + + # Group: Main control/service + kind = "service" if plan.base_class == "ft.Service" else "control" + lines.append(f" # Main {kind}") + lines.append(f' "{plan.control_name}",') + + # Group: Sub-controls + flat_subs = self._flatten_sub_controls(plan.sub_controls) + if flat_subs: + lines.append(" # Sub-controls") + for sub in flat_subs: + lines.append(f' "{sub.control_name}",') + + # Group: Sibling widgets + if plan.sibling_widgets: + lines.append(" # Sibling widgets") + for sibling in plan.sibling_widgets: + lines.append(f' "{sibling.control_name}",') + + # Group: Sub-modules + if plan.sub_modules: + lines.append(" # Sub-modules") + for sub in plan.sub_modules: + lines.append(f' "{sub.class_name}",') + + # Group: Debug console + if plan.include_console: + lines.append(" # Debug console") + lines.append(' "DebugConsole",') + lines.append(' "setup_logging",') + + # Group: Types and events + sub_control_names = {s.control_name for s in flat_subs} + sibling_names = {s.control_name for s in plan.sibling_widgets} + sub_module_names = {s.class_name for s in plan.sub_modules} + console_names = {"DebugConsole", "setup_logging"} if plan.include_console else set() + already_listed = ( + {plan.control_name} + | sub_control_names + | sibling_names + | sub_module_names + | console_names + ) + lines.append(" # Types and events") + for name in sorted(set(all_exports)): + if name not in already_listed: + lines.append(f' "{name}",') + + lines.append("]") + lines.append("") + + return {"__init__.py": "\n".join(lines)} + + @staticmethod + def _flatten_sub_controls(sub_controls: list[SubControlPlan]) -> list[SubControlPlan]: + """Flatten a recursive sub-control tree (leaves first, deduplicated). + + Args: + sub_controls: Top-level sub-control plans (may contain nested children). + + Returns: + Flat list with leaves before parents, no duplicates. + """ + result: list[SubControlPlan] = [] + seen: set[str] = set() + for sc in sub_controls: + for nested in PythonInitGenerator._flatten_sub_controls(sc.sub_controls): + if nested.control_name not in seen: + result.append(nested) + seen.add(nested.control_name) + if sc.control_name not in seen: + result.append(sc) + seen.add(sc.control_name) + return result diff --git a/src/flet_pkg/core/generators/python_submodule.py b/src/flet_pkg/core/generators/python_submodule.py new file mode 100644 index 0000000..1e4eaea --- /dev/null +++ b/src/flet_pkg/core/generators/python_submodule.py @@ -0,0 +1,229 @@ +"""Generator for Python sub-module files. + +Produces one file per sub-module (e.g., ``user.py``, ``notifications.py``) +following the composition pattern used in flet-onesignal. +""" + +from flet_pkg.core.generators.base import CodeGenerator +from flet_pkg.core.models import GenerationPlan, MethodPlan, SubModulePlan +from flet_pkg.core.parser import camel_to_snake + + +class PythonSubModuleGenerator(CodeGenerator): + """Generates Python sub-module files.""" + + def generate(self, plan: GenerationPlan) -> dict[str, str]: + """Generate one Python file per sub-module. + + Args: + plan: Generation plan produced by the analyzer. + + Returns: + Mapping of filename to generated source code. + """ + files: dict[str, str] = {} + for sub in plan.sub_modules: + filename = f"{sub.module_name}.py" + content = self._render_submodule(sub, plan) + files[filename] = content + return files + + def _render_submodule(self, sub: SubModulePlan, plan: GenerationPlan) -> str: + """Render a single sub-module file.""" + lines: list[str] = [] + control_snake = plan.control_name_snake or camel_to_snake(plan.control_name) + self._enum_names = {e.python_name for e in plan.enums} + + # Module docstring + mod_title = sub.module_name.replace("_", " ").title() + lines.append(f'"""\n{plan.control_name} {mod_title} module for {plan.package_name}.\n"""') + lines.append("") + + # Collect typing imports based on types used in methods + needs_any = False + needs_optional = False + enum_names = {e.python_name for e in plan.enums} + used_enums: set[str] = set() + + for method in sub.methods: + all_types = [method.return_type] + [p.python_type for p in method.params] + for t in all_types: + if "Any" in t: + needs_any = True + if "Optional" in t: + needs_optional = True + # Check if type references an enum + for en in enum_names: + if en in t: + used_enums.add(en) + + typing_imports = ["TYPE_CHECKING"] + if needs_any: + typing_imports.append("Any") + if needs_optional: + typing_imports.append("Optional") + + lines.append(f"from typing import {', '.join(sorted(typing_imports))}") + lines.append("") + + # Import enums used by methods + if used_enums: + enum_list = ", ".join(sorted(used_enums)) + lines.append(f"from {plan.package_name}.types import {enum_list}") + lines.append("") + + lines.append("if TYPE_CHECKING:") + lines.append(f" from {plan.package_name}.{control_snake} import {plan.control_name}") + lines.append("") + lines.append("") + + # Class + lines.append(f"class {sub.class_name}:") + lines.append(' """') + lines.append( + f" {plan.control_name} {sub.module_name.replace('_', ' ').title()} namespace." + ) + lines.append("") + if sub.docstring: + lines.append(f" {sub.docstring}") + lines.append(' """') + lines.append("") + + # Constructor + lines.append(f' def __init__(self, service: "{plan.control_name}"):') + lines.append(" self._service = service") + lines.append("") + + # Methods + for method in sub.methods: + lines.extend(self._render_method(method, sub)) + lines.append("") + + return "\n".join(lines) + + def _render_method(self, method: MethodPlan, sub: SubModulePlan) -> list[str]: + """Render a single async method definition for a sub-module class. + + Args: + method: Method plan with name, params, and return type. + sub: Parent sub-module plan for invoke key generation. + + Returns: + List of source code lines for the method. + """ + lines: list[str] = [] + + # Build signature + sig_parts = ["self"] + for p in method.params: + type_hint = p.python_type + if p.is_optional: + if "None" not in type_hint: + type_hint = f"{type_hint} | None" + default = self._py_default(p.default) + sig_parts.append(f"{p.python_name}: {type_hint} = {default}") + else: + sig_parts.append(f"{p.python_name}: {type_hint}") + + # Add timeout for getter-style methods (no params, return value) + has_timeout = False + if method.return_type not in ("None",) and not method.params: + sig_parts.append("timeout: float = 25") + has_timeout = True + + # All methods are async because they call _invoke_method + sig = ", ".join(sig_parts) + lines.append(f" async def {method.python_name}({sig}) -> {method.return_type}:") + + # Docstring + if method.docstring: + lines.append(' """') + for dl in method.docstring.split("\n"): + lines.append(f" {dl}" if dl.strip() else "") + if method.params: + lines.append("") + lines.append(" Args:") + for p in method.params: + if p.docstring: + lines.append(f" {p.python_name}: {p.docstring}") + else: + lines.append(f" {p.python_name}: {p.dart_name} parameter.") + lines.append(' """') + elif method.params: + doc = method.python_name.replace("_", " ").capitalize() + lines.append(f' """{doc}.') + lines.append("") + lines.append(" Args:") + for p in method.params: + if p.docstring: + lines.append(f" {p.python_name}: {p.docstring}") + else: + lines.append(f" {p.python_name}: {p.dart_name} parameter.") + lines.append(' """') + else: + doc = method.python_name.replace("_", " ").capitalize() + lines.append(f' """{doc}."""') + + # Build arguments dict using python_name as key (matches Dart dispatch) + # For enum-typed params, use .value to serialize (with None guard) + enum_names = getattr(self, "_enum_names", set()) + args_dict: dict[str, str] = {} + for p in method.params: + # Check if param type is an enum (strip Optional/nullable) + base_type = p.python_type.replace("Optional[", "").rstrip("]") + base_type = base_type.split("|")[0].strip() + if base_type in enum_names: + if p.is_optional or "None" in p.python_type: + args_dict[p.python_name] = ( + f"{p.python_name}.value if {p.python_name} is not None else None" + ) + else: + args_dict[p.python_name] = f"{p.python_name}.value" + else: + args_dict[p.python_name] = p.python_name + + if args_dict: + args_str = ", ".join(f'"{k}": {v}' for k, v in args_dict.items()) + invoke_args = f"{{{args_str}}}" + else: + invoke_args = "" + + # Method body + if method.return_type == "None": + if invoke_args: + lines.append( + f" await self._service._invoke_method(" + f'"{method.dart_method_name}", {invoke_args})' + ) + else: + lines.append( + f' await self._service._invoke_method("{method.dart_method_name}")' + ) + elif "bool" in method.return_type.lower(): + timeout_arg = ", timeout=timeout" if has_timeout else "" + if invoke_args: + lines.append( + f" result = await self._service._invoke_method(" + f'"{method.dart_method_name}", {invoke_args}{timeout_arg})' + ) + else: + lines.append( + f" result = await self._service._invoke_method(" + f'"{method.dart_method_name}"{timeout_arg})' + ) + lines.append(' return result == "true"') + else: + timeout_arg = ", timeout=timeout" if has_timeout else "" + if invoke_args: + lines.append( + f" result = await self._service._invoke_method(" + f'"{method.dart_method_name}", {invoke_args}{timeout_arg})' + ) + else: + lines.append( + f" result = await self._service._invoke_method(" + f'"{method.dart_method_name}"{timeout_arg})' + ) + lines.append(" return result") + + return lines diff --git a/src/flet_pkg/core/generators/python_types.py b/src/flet_pkg/core/generators/python_types.py new file mode 100644 index 0000000..862c09a --- /dev/null +++ b/src/flet_pkg/core/generators/python_types.py @@ -0,0 +1,214 @@ +"""Generator for the Python types file. + +Produces ``types.py`` with enum classes and event dataclasses +that mirror the Dart SDK's types. +""" + +from flet_pkg.core.generators.base import CodeGenerator +from flet_pkg.core.models import GenerationPlan, SubControlPlan +from flet_pkg.core.parser import camel_to_snake + + +class PythonTypesGenerator(CodeGenerator): + """Generates types.py with enums and event dataclasses.""" + + @staticmethod + def _optional_type(field_type: str) -> str: + """Wrap a type in ``Optional[]``, avoiding redundant ``Optional[X | None]``. + + Args: + field_type: Python type annotation string. + + Returns: + Wrapped type string. + """ + if "| None" in field_type: + return field_type + return f"Optional[{field_type}]" + + def generate(self, plan: GenerationPlan) -> dict[str, str]: + """Generate ``types.py`` with enums, event dataclasses, and sub-control classes. + + Args: + plan: Generation plan produced by the analyzer. + + Returns: + Mapping of filename to generated source code. + """ + # Always generate types.py — the error event class is always + # imported by the main control file. + + lines: list[str] = [] + + # Module docstring + lines.append('"""') + lines.append(f"Types, enums, and dataclasses for {plan.package_name}.") + lines.append('"""') + lines.append("") + lines.append("from __future__ import annotations") + lines.append("") + + # Imports + lines.append("from dataclasses import dataclass") + if plan.enums: + lines.append("from enum import Enum") + lines.append("from typing import Optional") + lines.append("") + lines.append("import flet as ft") + lines.append("") + lines.append("") + + # Enums + for enum in plan.enums: + lines.append(f"class {enum.python_name}(Enum):") + if enum.docstring: + lines.extend(self._format_docstring(enum.docstring, " ")) + else: + lines.append(f' """{enum.python_name} enum."""') + lines.append("") + for val_name, val_value, val_doc in enum.values: + # Convert camelCase to UPPER_SNAKE_CASE (e.g. sdkUnavailable → SDK_UNAVAILABLE) + py_name = camel_to_snake(val_name).upper() + lines.append(f' {py_name} = "{val_value}"') + if val_doc: + lines.append(f' """{val_doc}"""') + else: + lines.append(f' """{val_name}."""') + lines.append("") + lines.append("") + + # Event dataclasses (deduplicate by class name — multiple events + # can share the same Stream type, e.g. onMessage and onMessageOpenedApp + # both use Stream) + seen_event_classes: set[str] = set() + for event in plan.events: + if event.event_class_name in seen_event_classes: + continue + seen_event_classes.add(event.event_class_name) + lines.append("") + lines.append("@dataclass") + lines.append(f'class {event.event_class_name}(ft.Event["{plan.control_name}"]):') + event_desc = event.dart_event_name.replace("_", " ") + lines.append(f' """Event fired when {event_desc} occurs."""') + lines.append("") + + for field_name, field_type in event.fields: + if field_type == "dict": + lines.append(f" {field_name}: dict | None = None") + elif field_type == "bool": + lines.append(f" {field_name}: bool = False") + else: + opt = self._optional_type(field_type) + lines.append(f" {field_name}: {opt} = None") + field_desc = field_name.replace("_", " ").capitalize() + lines.append(f' """{field_desc}."""') + lines.append("") + + # Sibling widget event dataclasses + for sibling in plan.sibling_widgets: + for event in sibling.events: + if event.event_class_name in seen_event_classes: + continue + seen_event_classes.add(event.event_class_name) + lines.append("") + lines.append("@dataclass") + lines.append(f'class {event.event_class_name}(ft.Event["{sibling.control_name}"]):') + event_desc = event.dart_event_name.replace("_", " ") + lines.append(f' """Event fired when {event_desc} occurs."""') + lines.append("") + + for field_name, field_type in event.fields: + if field_type == "dict": + lines.append(f" {field_name}: dict = None") + elif field_type == "bool": + lines.append(f" {field_name}: bool = False") + else: + opt = self._optional_type(field_type) + lines.append(f" {field_name}: {opt} = None") + field_desc = field_name.replace("_", " ").capitalize() + lines.append(f' """{field_desc}."""') + lines.append("") + + # Sub-control event dataclasses + for sub in self._flatten_sub_controls(plan.sub_controls): + for event in sub.events: + if event.event_class_name in seen_event_classes: + continue + seen_event_classes.add(event.event_class_name) + lines.append("") + lines.append("@dataclass") + lines.append(f'class {event.event_class_name}(ft.Event["{sub.control_name}"]):') + event_desc = event.dart_event_name.replace("_", " ") + lines.append(f' """Event fired when {event_desc} occurs."""') + lines.append("") + + for field_name, field_type in event.fields: + if field_type == "dict": + lines.append(f" {field_name}: dict | None = None") + elif field_type == "bool": + lines.append(f" {field_name}: bool = False") + else: + opt = self._optional_type(field_type) + lines.append(f" {field_name}: {opt} = None") + field_desc = field_name.replace("_", " ").capitalize() + lines.append(f' """{field_desc}."""') + lines.append("") + + # Stub data classes (re-exported types from platform_interface) + for stub in plan.stub_data_classes: + lines.append("") + lines.append("@dataclass") + lines.append(f"class {stub.python_name}:") + if stub.docstring: + lines.append(f' """{stub.docstring}"""') + else: + lines.append(f' """{stub.python_name} data class."""') + lines.append("") + for field_name, field_type in stub.fields: + if field_type == "dict": + lines.append(f" {field_name}: dict | None = None") + else: + opt = self._optional_type(field_type) + lines.append(f" {field_name}: {opt} = None") + lines.append("") + + # Error event (always generated) + lines.append("") + lines.append("@dataclass") + error_cls = plan.error_event_class or f"{plan.control_name}ErrorEvent" + lines.append(f'class {error_cls}(ft.Event["{plan.control_name}"]):') + lines.append(' """Event fired when an error occurs."""') + lines.append("") + lines.append(" method: Optional[str] = None") + lines.append(' """The method that caused the error."""') + lines.append("") + lines.append(" message: Optional[str] = None") + lines.append(' """The error message."""') + lines.append("") + lines.append(" stack_trace: Optional[str] = None") + lines.append(' """The stack trace, if available."""') + lines.append("") + + return {"types.py": "\n".join(lines)} + + @staticmethod + def _flatten_sub_controls(sub_controls: list[SubControlPlan]) -> list[SubControlPlan]: + """Flatten a recursive sub-control tree (leaves first, deduplicated). + + Args: + sub_controls: Top-level sub-control plans (may contain nested children). + + Returns: + Flat list with leaves before parents, no duplicates. + """ + result: list[SubControlPlan] = [] + seen: set[str] = set() + for sc in sub_controls: + for nested in PythonTypesGenerator._flatten_sub_controls(sc.sub_controls): + if nested.control_name not in seen: + result.append(nested) + seen.add(nested.control_name) + if sc.control_name not in seen: + result.append(sc) + seen.add(sc.control_name) + return result diff --git a/src/flet_pkg/core/models.py b/src/flet_pkg/core/models.py new file mode 100644 index 0000000..3724002 --- /dev/null +++ b/src/flet_pkg/core/models.py @@ -0,0 +1,286 @@ +""" +Data models for Dart API parsing and code generation planning. + +This module defines two layers of models: +- **Dart API models** (DartParam, DartMethod, etc.): Represent the parsed Dart source code. +- **Generation plan models** (ParamPlan, MethodPlan, etc.): + Represent the Python/Dart code to generate. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +# --------------------------------------------------------------------------- +# Dart API Models (output of parser) +# --------------------------------------------------------------------------- + + +@dataclass +class DartParam: + """A parameter from a Dart method signature.""" + + name: str + dart_type: str = "dynamic" + required: bool = False + named: bool = False + default: str | None = None + docstring: str = "" + + +@dataclass +class DartMethod: + """A method from a Dart class.""" + + name: str + return_type: str = "void" + params: list[DartParam] = field(default_factory=list) + docstring: str = "" + is_static: bool = False + is_getter: bool = False + is_setter: bool = False + is_async: bool = False + + +@dataclass +class DartEnum: + """A Dart enum declaration.""" + + name: str + values: list[tuple[str, str]] = field(default_factory=list) + """(value_name, docstring) pairs.""" + docstring: str = "" + + +@dataclass +class DartClass: + """A parsed Dart class with its methods, properties, and metadata.""" + + name: str + methods: list[DartMethod] = field(default_factory=list) + constructor_params: list[DartParam] = field(default_factory=list) + """Constructor parameters (for widget classes in ui_control mode).""" + docstring: str = "" + parent_class: str = "" + source_file: str = "" + + +@dataclass +class DartPackageAPI: + """Container for the entire parsed Dart package API.""" + + classes: list[DartClass] = field(default_factory=list) + enums: list[DartEnum] = field(default_factory=list) + helper_classes: list[DartClass] = field(default_factory=list) + typedefs: dict[str, str] = field(default_factory=dict) + reexported_types: dict[str, str] = field(default_factory=dict) + """Mapping of type name → source package for re-exported public API types.""" + top_level_functions: list[DartMethod] = field(default_factory=list) + """Top-level functions (not inside any class) from the Dart package.""" + component_classes: list[DartClass] = field(default_factory=list) + """Non-widget classes referenced as constructor param types by widgets.""" + + +# --------------------------------------------------------------------------- +# Generation Plan Models (output of analyzer, input to generators) +# --------------------------------------------------------------------------- + + +@dataclass +class ParamPlan: + """Plan for a single method parameter.""" + + python_name: str + python_type: str = "Any" + dart_name: str = "" + dart_type: str = "dynamic" + is_optional: bool = False + is_named: bool = False + default: str | None = None + docstring: str = "" + + def __post_init__(self): + if not self.dart_name: + self.dart_name = self.python_name + + +@dataclass +class MethodPlan: + """Plan for generating a single Python async method.""" + + python_name: str + dart_method_name: str = "" + params: list[ParamPlan] = field(default_factory=list) + return_type: str = "None" + docstring: str = "" + is_async: bool = True + is_getter: bool = False + dart_original_name: str = "" + dart_class_name: str = "" + + def __post_init__(self): + if not self.dart_method_name: + self.dart_method_name = self.python_name + if not self.dart_original_name: + self.dart_original_name = self.python_name + + +@dataclass +class PropertyPlan: + """Plan for a control property (dataclass field).""" + + python_name: str + python_type: str = "str" + default_value: str = '""' + docstring: str = "" + dart_name: str = "" + """Original camelCase Dart parameter name. + + Used by the Dart widget generator to emit correct constructor calls. + Falls back to ``python_name`` when empty. + """ + dart_pre_init_call: str = "" + """Optional Dart code template for pre-init setter call. + + Use ``{var}`` placeholder for the variable name. + Example: ``"OneSignal.Debug.setLogLevel(OSLogLevel.values[{var}]);"`` + """ + dart_getter: str = "" + """Dart getter expression for UI controls. + + Example: ``control.getAlignment("alignment")``. + Populated by the analyzer for ``ui_control`` extensions. + """ + + +@dataclass +class EventPlan: + """Plan for an event handler attribute.""" + + python_attr_name: str + event_class_name: str + dart_event_name: str = "" + fields: list[tuple[str, str]] = field(default_factory=list) + dart_listener_method: str = "" + dart_sdk_accessor: str = "" + + def __post_init__(self): + if not self.dart_event_name: + self.dart_event_name = self.python_attr_name.removeprefix("on_") + + +@dataclass +class EnumPlan: + """Plan for generating a Python Enum class.""" + + python_name: str + values: list[tuple[str, str, str]] = field(default_factory=list) + """(PYTHON_NAME, "value", "docstring") triples.""" + docstring: str = "" + + +@dataclass +class SubModulePlan: + """Plan for a sub-module file (e.g., user.py, notifications.py).""" + + module_name: str + class_name: str + dart_prefix: str = "" + methods: list[MethodPlan] = field(default_factory=list) + docstring: str = "" + dart_class_name: str = "" + dart_sdk_accessor: str = "" + + +@dataclass +class StubDataClass: + """Stub for a re-exported data class from platform_interface.""" + + python_name: str + fields: list[tuple[str, str]] = field(default_factory=list) + """(field_name, field_type) pairs.""" + docstring: str = "" + + +@dataclass +class SubControlPlan: + """Plan for a sub-control class (compound widget child).""" + + control_name: str + """PascalCase name for the generated class (e.g. ``ActionPane``).""" + dart_class_name: str + """Original Dart class name (e.g. ``ActionPane``).""" + properties: list[PropertyPlan] = field(default_factory=list) + events: list[EventPlan] = field(default_factory=list) + parent_property: str = "" + """snake_case property name on the parent (e.g. ``start_action_pane``).""" + is_list: bool = False + """True when the parent references this as ``List``.""" + sub_controls: list[SubControlPlan] = field(default_factory=list) + """Nested sub-controls (recursive, max depth 3).""" + depth: int = 1 + """Nesting depth (1 = direct child of main widget).""" + + +@dataclass +class WidgetVariant: + """A variant in a widget family (e.g. SpinKitCircle, SpinKitHourGlass).""" + + dart_class_name: str + """Original Dart class name (e.g. ``SpinKitCircle``).""" + enum_value: str + """Snake_case enum value (e.g. ``circle``).""" + + +@dataclass +class SiblingWidgetPlan: + """A sibling widget — a separate control in the same package. + + Used when a Flutter package exposes multiple distinct widgets + with low parameter overlap (e.g. CircularPercentIndicator vs + LinearPercentIndicator). + """ + + control_name: str + """PascalCase name for the generated class.""" + dart_class_name: str + """Original Dart class name.""" + properties: list[PropertyPlan] = field(default_factory=list) + events: list[EventPlan] = field(default_factory=list) + sub_controls: list[SubControlPlan] = field(default_factory=list) + control_name_snake: str = "" + """Snake_case name for filenames.""" + + +@dataclass +class GenerationPlan: + """Complete plan for code generation. Fed to generators.""" + + control_name: str + package_name: str + base_class: str = "ft.Service" + description: str = "" + flutter_package: str = "" + properties: list[PropertyPlan] = field(default_factory=list) + main_methods: list[MethodPlan] = field(default_factory=list) + events: list[EventPlan] = field(default_factory=list) + sub_modules: list[SubModulePlan] = field(default_factory=list) + enums: list[EnumPlan] = field(default_factory=list) + stub_data_classes: list[StubDataClass] = field(default_factory=list) + """Stub data classes for re-exported types from platform_interface.""" + dart_import: str = "" + dart_listeners: list[dict[str, str]] = field(default_factory=list) + dart_main_class: str = "" + control_name_snake: str = "" + error_event_class: str = "" + """Name of the error event class (e.g. ``OSErrorEvent``).""" + sub_controls: list[SubControlPlan] = field(default_factory=list) + """Sub-control plans for compound widgets (e.g. ActionPane for Slidable).""" + include_console: bool = True + """Whether to include the debug console module.""" + widget_family_variants: list[WidgetVariant] = field(default_factory=list) + """Variants for widget family pattern (e.g. SpinKit → 30 variants).""" + widget_family_enum: EnumPlan | None = None + """Enum plan for the widget family type selector.""" + sibling_widgets: list[SiblingWidgetPlan] = field(default_factory=list) + """Sibling widget plans for multi-widget packages.""" diff --git a/src/flet_pkg/core/parser.py b/src/flet_pkg/core/parser.py new file mode 100644 index 0000000..35c3394 --- /dev/null +++ b/src/flet_pkg/core/parser.py @@ -0,0 +1,1511 @@ +""" +Dart source code parser. + +Parses Dart package source files to extract classes, methods, enums, +and their metadata into structured ``DartPackageAPI`` models. +""" + +import keyword +import re +from pathlib import Path + +from flet_pkg.core.models import ( + DartClass, + DartEnum, + DartMethod, + DartPackageAPI, + DartParam, +) + +UI_METHODS = { + "build", + "create", + "initState", + "dispose", + "debugFillProperties", + "didChangeDependencies", + "didUpdateWidget", + "setState", + "toString", + "noSuchMethod", + "lifecycleInit", + "MethodChannel", + "jsonRepresentation", + "convertToJsonString", + "if", + "if_", + "that", + "and_", + "createState", + "fromJson", + "toJson", + "toJsonString", + "jsonEncode", + "instance", + "callback", + "refuseInCallingOnInvitationReceived", + # Dart built-in / internal methods that are NOT public API + "compute", # dart:async compute() for isolate spawning + "identical", # dart:core identical() object identity check + "addIfNotNull", # internal utility for building argument maps + "getHashCode", # Object.hashCode accessor (also filtered as hashCode) + "hashCode", # Object.hashCode + "getFromJsonFunction", # internal JSON serialization helper + "runtimeType", # Object.runtimeType + "toMap", # internal serialization + "copyWith", # internal copy constructor + "compareTo", # Comparable.compareTo + "removeDuplicates", # internal dedup utility + "fromJsonFunction", # internal serialization factory +} + +LIFECYCLE_METHODS = { + "onLoad", + "onMount", + "onRemove", + "onChildrenChanged", + "onGameResize", + "update", + "render", + "dispose", + "renderDebugMode", + "renderTree", + "updateTree", +} + +# Classes whose name ENDS with these suffixes are skipped as main API classes. +UI_CLASS_SUFFIXES = ( + "Widget", + "Control", + "Delegate", + "Manager", + "View", + "Page", + "Overlay", + "Config", + "Protocol", + "Private", + "Impl", + "Guard", + "Foreground", + "Background", + "Internal", + "Instance", + "ServiceAPIPrivateImpl", + "Utils", + "Painter", + "Positioned", +) + +# Suffixes that indicate internal widgets (not user-facing) when parsing +# widget classes for ui_control extensions. "Widget" is intentionally +# excluded — many packages name their main user-facing class FooWidget +# (e.g. RiveWidget, VideoWidget). UI_CLASS_SUFFIXES is still used by +# _should_skip_class() for the service path. +_INTERNAL_WIDGET_SUFFIXES = ( + "Delegate", + "Painter", + "Positioned", + "Manager", + "Guard", + "Internal", + "Private", + "Impl", + "Utils", +) + +# Dart keywords that the method regex might capture as a "return type" +# when matching method calls inside method bodies (e.g. `await foo(`). +_DART_KEYWORDS = frozenset( + { + "await", + "return", + "final", + "var", + "const", + "throw", + "if", + "else", + "for", + "while", + "try", + "catch", + "new", + "yield", + "switch", + "case", + "break", + "continue", + "super", + "this", + "assert", + "class", + "enum", + "extends", + "implements", + "with", + "abstract", + "as", + "in", + "is", + } +) + +# Dart control-flow keywords that can be falsely parsed as method names. +# e.g. ``} catch (error, stackTrace) {`` matches the method regex as ``catch(…)``. +_DART_CONTROL_FLOW_NAMES = frozenset( + { + "catch", + "finally", + "then", + "rethrow", + } +) + +# Parent classes that indicate a platform implementation (not a public API). +# Classes extending these are skipped entirely. +PLATFORM_IMPL_BASES = ( + "Platform", + "PlatformInterface", + "MethodChannelPlatform", +) + +# Parent classes that indicate internal rendering/painting implementations. +_INTERNAL_PARENT_BASES = ( + "CustomPainter", + "BoxBorder", + "BoxDecoration", + "Decoration", + "ShapeBorder", +) + +# Parent classes that indicate a Flutter widget (not a service API). +WIDGET_BASES = ( + "StatelessWidget", + "StatefulWidget", + "InheritedWidget", + "RenderObjectWidget", + "LeafRenderObjectWidget", + "SingleChildRenderObjectWidget", + "MultiChildRenderObjectWidget", +) + +# Dart types that are Flutter-internal and should NOT become Flet properties. +# Note: "Widget" is intentionally excluded — for ui_control extensions, +# Widget maps to ft.Control (a valid child control property). +_WIDGET_INTERNAL_TYPES = { + "Key", + "BuildContext", + "GlobalKey", + "ValueKey", + "ObjectKey", + "UniqueKey", + "PageStorageKey", + "LocalKey", + "Animation", + "AnimationController", + "TickerProvider", + "ScrollController", + "FocusNode", + "TextEditingController", +} + +# Classes ending with these are parsed into helper_classes (not skipped entirely). +# Used for event field extraction, type resolution, and config data classes. +HELPER_CLASS_SUFFIXES = ( + "Event", + "Data", + "Result", + "State", + "ChangedState", + "ClickResult", + "Button", + "Layout", + "Configuration", + "Config", + "Options", + "Params", + "Info", + "Style", + "Position", + "Gradient", + "Animation", +) + +# Parent classes that indicate a data/serialization class (not an API). +# Classes extending these are treated as helper classes. +DATA_MODEL_BASES = ( + "JSONStringRepresentable", + "Serializable", + "JsonModel", +) + + +def camel_to_snake(name: str) -> str: + """Convert ``CamelCase`` or ``camelCase`` to ``snake_case``. + + Handles consecutive uppercase letters (acronyms) properly: + ``JWT`` -> ``jwt``, ``loginWithJWT`` -> ``login_with_jwt``, + ``ABCDef`` -> ``abc_def``. + """ + # Handle consecutive uppercase (acronyms): ABCDef -> ABC_Def + s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name) + # Handle transitions from lowercase/digit to uppercase: camelCase -> camel_Case + s = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s) + return s.lower() + + +def _should_skip_class(class_name: str, docstring: str = "", parent_class: str = "") -> bool: + """Check if a class should be skipped entirely (not parsed at all).""" + if class_name.startswith("_"): + return True + if class_name.endswith(UI_CLASS_SUFFIXES): + return True + if "nodoc" in docstring.lower() or "internal" in docstring.lower(): + return True + if _is_platform_impl(parent_class): + return True + if parent_class in WIDGET_BASES or parent_class in _INTERNAL_PARENT_BASES: + return True + # Platform-specific Plugin subclass: e.g. AndroidFlutterLocalNotificationsPlugin + # extends MethodChannelFlutterLocalNotificationsPlugin → skip. + # But keep the main Plugin class (no parent or non-Plugin parent). + if class_name.endswith("Plugin") and parent_class.endswith("Plugin"): + return True + return False + + +def _is_platform_impl(parent_class: str) -> bool: + """Check if a parent class indicates a platform implementation.""" + if not parent_class: + return False + # Specific suffixes that always indicate platform interfaces + if parent_class.endswith(("PlatformInterface", "MethodChannelPlatform")): + return True + # FooPlatform is a platform impl, but FooPluginPlatform (e.g. + # FirebasePluginPlatform) is a generic SDK base — keep children. + if parent_class.endswith("Platform") and "Plugin" not in parent_class: + return True + return False + + +def _is_helper_class(class_name: str, parent_class: str = "") -> bool: + """Check if a class is a helper/data class. + + These are parsed into helper_classes for event field extraction. + Detected by suffix (Event, Data, Result, State, etc.) or by extending + known serialization base classes. + """ + if class_name.endswith(HELPER_CLASS_SUFFIXES): + return True + if parent_class in DATA_MODEL_BASES: + return True + return False + + +def _should_skip_method( + method_name: str, + docstring: str = "", + return_type: str = "", +) -> bool: + return ( + method_name in UI_METHODS + or method_name in LIFECYCLE_METHODS + or method_name in _DART_CONTROL_FLOW_NAMES + or method_name.startswith("_") + # Skip on* methods UNLESS they are Stream getters (event sources) + or (method_name.startswith("on") and not return_type.startswith("Stream")) + or "nodoc" in docstring.lower() + or not method_name.isidentifier() + or method_name[0].isupper() + ) + + +def _is_inside_comment(text: str, pos: int) -> bool: + """Check if *pos* falls inside a ``//`` or ``///`` comment.""" + line_start = text.rfind("\n", 0, pos) + 1 + line_prefix = text[line_start:pos] + return "//" in line_prefix + + +def _is_inside_string(text: str, pos: int) -> bool: + """Check if *pos* falls inside a string literal (single or double quoted).""" + # Scan from the last newline to pos, counting unescaped quotes + line_start = text.rfind("\n", 0, pos) + 1 + line = text[line_start:pos] + in_single = False + in_double = False + i = 0 + while i < len(line): + c = line[i] + if c == "\\" and (in_single or in_double): + i += 2 + continue + if c == "'" and not in_double: + in_single = not in_single + elif c == '"' and not in_single: + in_double = not in_double + i += 1 + return in_single or in_double + + +def _extract_balanced_parens(text: str, open_pos: int) -> str | None: + """Extract content between balanced parentheses starting at *open_pos*. + + ``text[open_pos]`` must be ``(``. Returns the text between the outer + parens (exclusive) or ``None`` if unbalanced. + """ + if open_pos >= len(text) or text[open_pos] != "(": + return None + depth = 1 + i = open_pos + 1 + length = len(text) + while i < length: + c = text[i] + # Skip string literals (single and double quotes) + if c in ("'", '"'): + quote = c + i += 1 + while i < length and text[i] != quote: + if text[i] == "\\": + i += 1 # skip escaped char + i += 1 + i += 1 # skip closing quote + continue + # Skip line comments + if c == "/" and i + 1 < length and text[i + 1] == "/": + i = text.find("\n", i) + if i == -1: + break + i += 1 + continue + # Skip block comments + if c == "/" and i + 1 < length and text[i + 1] == "*": + i = text.find("*/", i + 2) + if i == -1: + break + i += 2 + continue + # Track nesting for all bracket types + if c in "(<[": + depth += 1 + elif c in ")>]": + depth -= 1 + if depth == 0: + return text[open_pos + 1 : i] + i += 1 + return None + + +def _clean_docstring(match: re.Match) -> str: + """Clean a Dart docstring, preserving paragraphs, lists, and code blocks.""" + doc = match.group(1) or match.group(2) or "" + # Remove /** ... */ markers + doc = re.sub(r"^/\*\*\s*", "", doc) + doc = re.sub(r"\s*\*/$", "", doc) + doc = re.sub(r"^\s*\*\s?", "", doc, flags=re.MULTILINE) + + lines = doc.split("\n") + cleaned = [re.sub(r"^\s*///+\s?", "", ln) for ln in lines] + + result_parts: list[str] = [] + paragraph: list[str] = [] + in_code = False + + def flush(): + if paragraph: + result_parts.append(re.sub(r" +", " ", " ".join(paragraph)).strip()) + paragraph.clear() + + for line in cleaned: + s = line.rstrip() + if s.startswith("```"): + flush() + result_parts.append(s) + in_code = not in_code + continue + if in_code: + result_parts.append(line.rstrip()) + continue + if not s: + flush() + result_parts.append("") + continue + if re.match(r"^\s*[-*]\s", s): + flush() + result_parts.append(s) + continue + paragraph.append(s) + + flush() + text = "\n".join(result_parts).strip() + return re.sub(r"\n{3,}", "\n\n", text) + + +def _clean_enum_value_doc(lines: list[str]) -> str: + """Collapse /// doc-comment lines above an enum value into a single line.""" + if not lines: + return "" + return re.sub(r" +", " ", " ".join(ln.strip() for ln in lines)).strip() + + +def _extract_param_docs(params_raw: str) -> dict[str, str]: + """Extract per-parameter docstrings from /// comments within a param list.""" + docs: dict[str, str] = {} + pending: list[str] = [] + for line in params_raw.split("\n"): + stripped = line.strip() + doc_m = re.match(r"^///+\s?(.*)", stripped) + if doc_m: + pending.append(doc_m.group(1)) + continue + if pending and stripped: + clean = re.sub(r"//.*$", "", stripped).strip().rstrip(",").strip() + clean = re.sub(r"@\w+(\([^)]*\))?\s*", "", clean).strip() + clean = re.sub(r"^required\s+", "", clean).strip() + clean = re.sub(r"\s*=\s*.*$", "", clean).strip() + parts = clean.split() + if parts: + name = parts[-1].strip("?,") + if name.isidentifier(): + docs[name] = re.sub(r" +", " ", " ".join(pending)).strip() + pending = [] + elif not stripped: + pending = [] + return docs + + +def _extract_code_block(content: str) -> str | None: + """Extract text inside a ``{ ... }`` block, handling comments and strings. + + ``content`` must start with the opening ``{``. Returns the text + between the braces (exclusive) or ``None`` if the block is never closed. + """ + brace_count = 1 + i = 1 + length = len(content) + while i < length: + c = content[i] + + # Skip single-line comments ( // ... \n ) + if c == "/" and i + 1 < length and content[i + 1] == "/": + i = content.find("\n", i) + if i == -1: + break + i += 1 + continue + + # Skip multi-line comments ( /* ... */ ) + if c == "/" and i + 1 < length and content[i + 1] == "*": + i = content.find("*/", i + 2) + if i == -1: + break + i += 2 + continue + + # Skip string literals (single-quoted, double-quoted, triple-quoted) + if c in ("'", '"'): + # Check for triple-quote + if i + 2 < length and content[i + 1] == c and content[i + 2] == c: + end = content.find(c * 3, i + 3) + if end == -1: + break + i = end + 3 + continue + # Single-quoted string: advance past escaped chars + i += 1 + while i < length and content[i] != c: + if content[i] == "\\": + i += 1 # skip escaped char + i += 1 + i += 1 # skip closing quote + continue + + if c == "{": + brace_count += 1 + elif c == "}": + brace_count -= 1 + if brace_count == 0: + return content[1:i] + i += 1 + return None + + +def _parse_param(param_str: str, is_named_section: bool) -> DartParam | None: + """Parse a single parameter string into a DartParam.""" + param_str = param_str.strip() + if not param_str: + return None + + required = False + if param_str.startswith("required "): + required = True + param_str = param_str[len("required ") :].strip() + + # Extract default value + default = None + default_match = re.match(r"^(.+?)\s*=\s*(.+)$", param_str) + if default_match: + param_str = default_match.group(1).strip() + default = default_match.group(2).strip() + + # Remove annotations like @Deprecated + param_str = re.sub(r"@\w+(\([^)]*\))?\s*", "", param_str).strip() + + parts = param_str.split() + if len(parts) < 2: + # No type annotation, just a name + name = parts[0] if parts else "" + if not name or not name.isidentifier(): + return None + return DartParam( + name=name, + dart_type="dynamic", + required=required and not default, + named=is_named_section, + default=default, + ) + + dart_type = " ".join(parts[:-1]) + name = parts[-1] + + if not name.isidentifier(): + return None + + return DartParam( + name=name, + dart_type=dart_type, + required=required and not default, + named=is_named_section, + default=default, + ) + + +def _parse_params_string(params_raw: str) -> list[DartParam]: + """Parse a full Dart parameter list string into DartParam objects.""" + # Capture per-parameter /// docstrings BEFORE stripping them + param_docs = _extract_param_docs(params_raw) + # Strip inline /// and // doc comments that appear between parameters + # (e.g. firebase_messaging's requestPermission has /// on each param). + params_raw = re.sub(r"///[^\n]*", "", params_raw) + params_raw = re.sub(r"//[^\n]*", "", params_raw) + params_raw = params_raw.strip() + if not params_raw: + return [] + + result: list[DartParam] = [] + + # Detect named params section: { ... } + # and positional params section: [ ... ] + # Track nesting depth so that brackets inside default values (e.g., + # `const [...]` or `{key: value}`) don't corrupt section markers. + is_named = False + is_optional = False + nesting = 0 # depth inside (), <>, or [] within defaults + clean = [] + for char in params_raw: + if nesting == 0: + if char == "{": + is_named = True + # Retroactively mark trailing whitespace as named + # so that spaces between `,` and `{` don't misclassify + # the next parameter as positional. + i = len(clean) - 1 + while i >= 0 and clean[i][0].isspace(): + clean[i] = (clean[i][0], True, clean[i][2]) + i -= 1 + continue + if char == "}": + is_named = False + continue + if char == "[" and not is_named: + is_optional = True + continue + if char == "]" and is_optional and not is_named: + is_optional = False + continue + # Track nesting for all bracket types inside defaults + if char in "(<[": + nesting += 1 + elif char in ")>]": + nesting -= 1 + clean.append((char, is_named, is_optional)) + + # Rebuild with markers + segments: list[tuple[str, bool]] = [] + current_chars: list[str] = [] + current_named = False + for char, named, _opt in clean: + if not current_chars: + current_named = named + if char == "," and _count_depth(current_chars) == 0: + segments.append(("".join(current_chars).strip(), current_named)) + current_chars = [] + current_named = named + else: + current_chars.append(char) + if current_chars: + segments.append(("".join(current_chars).strip(), current_named)) + + for seg, named in segments: + param = _parse_param(seg, named) + if param: + if param.name in param_docs: + param.docstring = param_docs[param.name] + result.append(param) + + return result + + +def _count_depth(chars: list[str]) -> int: + depth = 0 + for c in chars: + if c in "(<[": + depth += 1 + elif c in ")>]": + depth -= 1 + return depth + + +# --------------------------------------------------------------------------- +# Typedef parsing +# --------------------------------------------------------------------------- + +# Matches: typedef void TypedefName(ParamType param); +# Also: typedef void TypedefName(ParamType param, ...) +_TYPEDEF_RE = re.compile( + r"typedef\s+\w+\s+(\w+)\s*\(([^)]*)\)\s*;", +) + + +def _parse_typedefs(content: str) -> dict[str, str]: + """Extract typedef declarations and map typedef name to first param type. + + Returns a dict mapping typedef name -> first parameter type. + For example: ``OnNotificationClickListener`` -> ``OSNotificationClickEvent`` + """ + typedefs: dict[str, str] = {} + for match in _TYPEDEF_RE.finditer(content): + typedef_name = match.group(1) + params_str = match.group(2).strip() + if not params_str: + continue + # Get the first parameter's type + first_param = params_str.split(",")[0].strip() + parts = first_param.split() + if len(parts) >= 2: + # "ParamType paramName" → extract ParamType + param_type = parts[0] + typedefs[typedef_name] = param_type + elif len(parts) == 1 and parts[0][0].isupper(): + # Just a type name without param name + typedefs[typedef_name] = parts[0] + return typedefs + + +# --------------------------------------------------------------------------- +# Enum parsing +# --------------------------------------------------------------------------- + +_ENUM_HEAD_RE = re.compile( + r"(?:///[^\n]*\n|/\*\*.*?\*/\s*)?" + r"enum\s+(\w+)\s*\{", + re.DOTALL, +) + + +def _parse_enums(content: str) -> list[DartEnum]: + """Extract enum declarations from Dart source.""" + enums: list[DartEnum] = [] + for match in _ENUM_HEAD_RE.finditer(content): + name = match.group(1) + # Use balanced brace extraction to handle enums with method bodies + open_pos = match.end() - 1 # position of the '{' + body = _extract_balanced_braces(content, open_pos) + if body is None: + continue + + # Extract docstring before enum + doc_match = re.search( + r"(///[^\n]*(?:\n///[^\n]*)*|/\*\*.*?\*/)\s*$", + content[: match.start()], + re.DOTALL, + ) + docstring = _clean_docstring(doc_match) if doc_match else "" + + # Parse values with per-value docstrings from /// comments + values_part = body.split(";")[0] + values: list[tuple[str, str]] = [] + pending_doc: list[str] = [] + for line in values_part.split("\n"): + stripped = line.strip() + doc_m = re.match(r"^///+\s?(.*)", stripped) + if doc_m: + pending_doc.append(doc_m.group(1)) + continue + stripped = re.sub(r"//.*$", "", stripped).strip() + for piece in stripped.split(","): + val = piece.strip() + val = re.sub(r"\(.*\)$", "", val).strip() + if val and val.isidentifier() and not val.startswith("_"): + values.append((val, _clean_enum_value_doc(pending_doc))) + pending_doc = [] + elif not val: + pass + else: + pending_doc = [] + + if values: + enums.append(DartEnum(name=name, values=values, docstring=docstring)) + + return enums + + +def _extract_balanced_braces(text: str, open_pos: int) -> str | None: + """Extract content between balanced braces starting at open_pos.""" + if open_pos >= len(text) or text[open_pos] != "{": + return None + depth = 1 + i = open_pos + 1 + while i < len(text) and depth > 0: + if text[i] == "{": + depth += 1 + elif text[i] == "}": + depth -= 1 + i += 1 + if depth != 0: + return None + return text[open_pos + 1 : i - 1] + + +# --------------------------------------------------------------------------- +# Widget constructor parameter parsing +# --------------------------------------------------------------------------- + + +def _parse_constructor_params(class_name: str, class_block: str) -> list[DartParam]: + """Extract constructor parameters from a Dart widget class. + + Builds a field-type map from ``final Type fieldName;`` declarations, + then finds the primary constructor and resolves ``this.fieldName`` + references to their declared types. + + Skips Flutter-internal types (Widget, Key, etc.) that cannot be + serialized as Flet control properties. + """ + # Step 1: Build field type map from `final Type fieldName;` declarations + field_types: dict[str, str] = {} + field_docs: dict[str, str] = {} + for m in re.finditer( + r"((?:///[^\n]*\n\s*)*)" + r"(?:late\s+)?final\s+" + r"([\w]+(?:<[^;]*?>)?(?:\?)?)\s+" + r"(\w+)\s*(?:=[^;]*)?;", + class_block, + ): + doc_lines = m.group(1).strip() + raw_type = m.group(2).strip() + field_name = m.group(3) + if not field_name.startswith("_"): + field_types[field_name] = raw_type + if doc_lines: + cleaned_parts = [re.sub(r"^\s*///+\s?", "", ln) for ln in doc_lines.split("\n")] + field_docs[field_name] = re.sub(r" +", " ", " ".join(cleaned_parts)).strip() + + # Step 2: Find the primary constructor + ctor_re = re.compile( + r"(?:const\s+)?" + re.escape(class_name) + r"\s*\(", + ) + ctor_match = ctor_re.search(class_block) + if not ctor_match: + return [] + + params_raw = _extract_balanced_parens(class_block, ctor_match.end() - 1) + if params_raw is None: + return [] + + # Capture per-parameter /// docstrings BEFORE stripping them + ctor_param_docs = _extract_param_docs(params_raw) + + # Strip all comments (// and ///) and template tags ({@...}) + params_raw = re.sub(r"//[^\n]*", "", params_raw) + params_raw = re.sub(r"\{@\w+[^}]*\}", "", params_raw) + + # Step 3: Parse constructor params (handling this. references) + result: list[DartParam] = [] + is_named = False + + # Split respecting nesting + segments: list[tuple[str, bool]] = [] + current: list[str] = [] + depth = 0 + for char in params_raw: + if depth == 0: + if char == "{": + is_named = True + continue + if char == "}": + is_named = False + continue + if char in "(<[": + depth += 1 + elif char in ")>]": + depth -= 1 + if char == "," and depth == 0: + segments.append(("".join(current).strip(), is_named)) + current = [] + else: + current.append(char) + if current: + segments.append(("".join(current).strip(), is_named)) + + for seg, named in segments: + seg = seg.strip() + if not seg: + continue + # Strip annotations + seg = re.sub(r"@\w+(\([^)]*\))?\s*", "", seg).strip() + + required = False + if seg.startswith("required "): + required = True + seg = seg[len("required ") :].strip() + + # Extract default value + default = None + # Match default respecting nesting + eq_depth = 0 + eq_pos = -1 + for i, c in enumerate(seg): + if c in "(<[": + eq_depth += 1 + elif c in ")>]": + eq_depth -= 1 + elif c == "=" and eq_depth == 0: + eq_pos = i + break + if eq_pos >= 0: + default = seg[eq_pos + 1 :].strip() + seg = seg[:eq_pos].strip() + + # Handle `this.fieldName` syntax + this_match = re.match(r"this\.(\w+)$", seg) + if this_match: + field_name = this_match.group(1) + dart_type = field_types.get(field_name, "dynamic") + else: + # Explicit type: `Type fieldName` + parts = seg.split() + if len(parts) >= 2: + dart_type = " ".join(parts[:-1]) + field_name = parts[-1] + elif len(parts) == 1: + field_name = parts[0] + dart_type = field_types.get(field_name, "dynamic") + else: + continue + + if not field_name.isidentifier(): + continue + + # Skip Flutter-internal types + base_type = re.sub(r"[?<].*", "", dart_type).strip() + if base_type in _WIDGET_INTERNAL_TYPES: + continue + + # Resolve docstring: ctor param doc takes priority, then field doc + doc = ctor_param_docs.get(field_name, "") or field_docs.get(field_name, "") + + result.append( + DartParam( + name=field_name, + dart_type=dart_type, + required=required and default is None, + named=named, + default=default, + docstring=doc, + ) + ) + + return result + + +# --------------------------------------------------------------------------- +# Class method parsing (shared between main and helper classes) +# --------------------------------------------------------------------------- + + +def _parse_class_methods(class_block: str, skip_filter: bool = True) -> list[DartMethod]: + """Parse methods from a class body block.""" + methods: list[DartMethod] = [] + seen: set[str] = set() + + # Parse methods with parentheses: `Type name(params)` or `Type get/set name(params)` + # Match up to the opening paren, then use balanced extraction for params. + method_sig_re = re.compile( + r"(?:@[^\n]*\n)*" + r"(static\s+)?" + r"(Future<.*?>|Future|void|String|bool|int|double|dynamic|" + r"Map<.*?>|List<.*?>|Set<.*?>|\w+(?:\?)?)\s+" + r"(get\s+|set\s+)?(\w+)" + r"\s*\(", + ) + method_matches = method_sig_re.finditer(class_block) + + for m in method_matches: + # Skip matches that fall inside a comment line + if _is_inside_comment(class_block, m.start()): + continue + + # Skip @Deprecated or @visibleForTesting annotated methods + annotation_text = m.group(0) + if re.search(r"@[Dd]eprecated|@visibleForTesting", annotation_text): + continue + + is_static = m.group(1) is not None + return_type = m.group(2) + accessor = m.group(3) + is_getter = accessor is not None and accessor.strip() == "get" + is_setter = accessor is not None and accessor.strip() == "set" + method_name = m.group(4) + + # Skip matches where the "return type" is actually a Dart keyword. + # This happens when the regex matches a method call inside a body: + # e.g. "await isSkinTemperatureAvailable(" → return_type="await" + if return_type.rstrip("?") in _DART_KEYWORDS: + continue + + # Extract balanced parameter text from opening paren + open_pos = m.end() - 1 # position of the '(' + params_raw = _extract_balanced_parens(class_block, open_pos) + if params_raw is None: + continue + + is_async = return_type.startswith("Future") + + method_doc_match = re.search( + r"(///[^\n]*(?:\n\s*///[^\n]*)*|/\*\*.*?\*/)\s*$", + class_block[: m.start()], + re.DOTALL, + ) + method_doc = _clean_docstring(method_doc_match) if method_doc_match else "" + + if method_name in seen: + continue + if skip_filter and _should_skip_method(method_name, method_doc, return_type): + continue + seen.add(method_name) + + safe_name = method_name + if keyword.iskeyword(method_name): + safe_name = method_name + "_" + + parsed_params = _parse_params_string(params_raw) + + methods.append( + DartMethod( + name=safe_name, + return_type=return_type if not is_setter else "void", + params=parsed_params, + docstring=method_doc, + is_static=is_static, + is_getter=is_getter, + is_setter=is_setter, + is_async=is_async, + ) + ) + + # Parse bare getters without parentheses: `Type get name { ... }` or `Type get name =>` + getter_matches = re.finditer( + r"(?:@[^\n]*\n)*" + r"(static\s+)?" + r"(Future<.*?>|Stream<.*?>|String|bool|int|double|dynamic|Map<.*?>|List<.*?>|\w+\??)\s+" + r"get\s+(\w+)\s*(?:\{|=>)", + class_block, + ) + for m in getter_matches: + if _is_inside_comment(class_block, m.start()): + continue + + is_static = m.group(1) is not None + return_type = m.group(2) + getter_name = m.group(3) + + if getter_name in seen: + continue + + method_doc_match = re.search( + r"(///[^\n]*(?:\n\s*///[^\n]*)*|/\*\*.*?\*/)\s*$", + class_block[: m.start()], + re.DOTALL, + ) + method_doc = _clean_docstring(method_doc_match) if method_doc_match else "" + + if skip_filter and _should_skip_method(getter_name, method_doc, return_type): + continue + seen.add(getter_name) + + methods.append( + DartMethod( + name=getter_name, + return_type=return_type, + params=[], + docstring=method_doc, + is_static=is_static, + is_getter=True, + is_async=return_type.startswith("Future"), + ) + ) + + # Parse instance fields (only for helper/data classes — skip_filter=False). + # Matches: `late? Type? fieldName;` or `Type? fieldName = default;` + if not skip_filter: + field_matches = re.finditer( + r"(?:///[^\n]*\n\s*)*" + r"(late\s+)?" + r"((?:final\s+)?)" + r"(String|bool|int|double|Map<[^>]*>|List<[^>]*>|[A-Z]\w*)" + r"(\??)[ \t]+" + r"(\w+)" + r"\s*(?:=[^;]*)?;", + class_block, + ) + for m in field_matches: + if _is_inside_comment(class_block, m.start()): + continue + field_type = m.group(3) + (m.group(4) or "") + field_name = m.group(5) + if field_name in seen or field_name.startswith("_"): + continue + seen.add(field_name) + + # Extract docstring for the field + field_doc_match = re.search( + r"(///[^\n]*(?:\n\s*///[^\n]*)*)\s*$", + class_block[: m.start()], + ) + field_doc = _clean_docstring(field_doc_match) if field_doc_match else "" + + methods.append( + DartMethod( + name=field_name, + return_type=field_type, + params=[], + docstring=field_doc, + is_getter=True, + is_async=False, + ) + ) + + if skip_filter: + methods = [ + m for m in methods if not _should_skip_method(m.name, m.docstring or "", m.return_type) + ] + + return methods + + +# --------------------------------------------------------------------------- +# Re-export parsing +# --------------------------------------------------------------------------- + + +def _parse_reexports(lib_dir: Path) -> dict[str, str]: + """Parse barrel files to find re-exported public API type names. + + Scans top-level ``export`` directives in ``lib/*.dart`` for + ``show`` clauses and returns a mapping of symbol name → source package. + """ + reexports: dict[str, str] = {} + # Only check top-level dart files in lib/ (barrel files) + for dart_file in lib_dir.glob("*.dart"): + content = dart_file.read_text(encoding="utf-8") + for m in re.finditer( + r"export\s+['\"]package:([^'\"]+)['\"]" + r"\s+show\s+([^;]+);", + content, + ): + package_path_str = m.group(1) + symbols = [s.strip() for s in m.group(2).split(",")] + package_name = package_path_str.split("/")[0] + for sym in symbols: + if sym and sym[0].isupper(): + reexports[sym] = package_name + return reexports + + +# --------------------------------------------------------------------------- +# Top-level function parsing +# --------------------------------------------------------------------------- + + +def _parse_top_level_functions( + content: str, + source_file: str = "", +) -> list[DartMethod]: + """Parse top-level functions (not inside any class) from Dart source. + + Finds function declarations at the top level of the file and extracts + their signatures. Skips private functions (starting with ``_``), + ``main()``, and functions inside class bodies. + """ + # Skip files that are clearly internal implementation + if source_file: + stem = source_file.rsplit("/", maxsplit=1)[-1].replace(".dart", "") + if any(stem.endswith(s) for s in ("_conversion", "_internal", "_stub", "_test")): + return [] + # First, find the spans of all class/enum bodies so we can exclude them + class_spans: list[tuple[int, int]] = [] + for m in re.finditer( + r"(?:abstract\s+)?(?:class|enum|mixin|extension)\s+\w+[^{]*\{", + content, + ): + block = _extract_code_block(content[m.end() - 1 :]) + if block is not None: + class_spans.append((m.start(), m.end() + len(block))) + + def _is_inside_class(pos: int) -> bool: + return any(start <= pos < end for start, end in class_spans) + + # Match top-level function declarations + func_re = re.compile( + r"(?:@[^\n]*\n)*" + r"(static\s+)?" + r"(Future<.*?>|Future|void|String|bool|int|double|dynamic|" + r"Map<.*?>|List<.*?>|Set<.*?>|\w+(?:\?)?)\s+" + r"(\w+)" + r"\s*\(", + ) + + functions: list[DartMethod] = [] + seen: set[str] = set() + + # Known valid Dart return types (lowercase) — catches false positives + # from matches inside string literals. + _VALID_RETURN_TYPES = { + "void", + "int", + "double", + "bool", + "dynamic", + "num", + "var", + } + + for m in func_re.finditer(content): + if _is_inside_class(m.start()): + continue + if _is_inside_comment(content, m.start()): + continue + if _is_inside_string(content, m.start()): + continue + + # Check for @Deprecated or @visibleForTesting annotations + annotation_text = m.group(0) + if re.search(r"@[Dd]eprecated|@visibleForTesting", annotation_text): + continue + + return_type = m.group(2) + func_name = m.group(3) + + # Skip private, main, and non-identifier names + if func_name.startswith("_") or func_name == "main": + continue + if not func_name.isidentifier() or func_name[0].isupper(): + continue + if func_name in seen: + continue + # Skip known non-function patterns (import, export, etc.) + if return_type in ("import", "export", "part", "library", "typedef"): + continue + # Skip Dart keywords captured as "return type" from method body calls + if return_type.rstrip("?") in _DART_KEYWORDS: + continue + # Validate return type: must be a known type or start with uppercase + if ( + return_type not in _VALID_RETURN_TYPES + and not return_type[0].isupper() + and not return_type.startswith("Future") + and not return_type.startswith("Stream") + and not return_type.startswith("Map") + and not return_type.startswith("List") + and not return_type.startswith("Set") + ): + continue + + # Extract balanced params + open_pos = m.end() - 1 + params_raw = _extract_balanced_parens(content, open_pos) + if params_raw is None: + continue + + # Docstring + doc_match = re.search( + r"(///[^\n]*(?:\n\s*///[^\n]*)*|/\*\*.*?\*/)\s*$", + content[: m.start()], + re.DOTALL, + ) + func_doc = _clean_docstring(doc_match) if doc_match else "" + + if _should_skip_method(func_name, func_doc): + continue + + seen.add(func_name) + parsed_params = _parse_params_string(params_raw) + is_async = return_type.startswith("Future") + + functions.append( + DartMethod( + name=func_name, + return_type=return_type, + params=parsed_params, + docstring=func_doc, + is_static=False, + is_getter=False, + is_setter=False, + is_async=is_async, + ) + ) + + return functions + + +# --------------------------------------------------------------------------- +# Main parse functions +# --------------------------------------------------------------------------- + + +def _collect_component_classes( + widget_classes: list[DartClass], + all_parsed: dict[str, tuple[str, str]], + helper_names: set[str], +) -> list[DartClass]: + """Identify non-widget classes referenced by widget constructor params. + + Scans widget constructor parameter types for class names that exist + in the parsed source but were not included as widgets or helpers. + These become candidates for sub-control generation. + """ + # Collect type names from widget constructor params + referenced: set[str] = set() + for widget in widget_classes: + for param in widget.constructor_params: + base = re.sub(r"[?<].*", "", param.dart_type).strip() + generic = re.search(r"<(\w+)>", param.dart_type) + if generic: + referenced.add(generic.group(1)) + if base: + referenced.add(base) + + # Filter: in same package (in all_parsed), not already a widget/helper + widget_names = {w.name for w in widget_classes} + components: list[DartClass] = [] + for name in sorted(referenced): + if name in widget_names or name in helper_names: + continue + if name not in all_parsed: + continue + block, src = all_parsed[name] + ctor_params = _parse_constructor_params(name, block) + if ctor_params: + components.append( + DartClass( + name=name, + constructor_params=ctor_params, + source_file=src, + ) + ) + return components + + +def detect_extension_type(package_path: Path) -> str: + """Detect whether a Flutter package is a service or UI control. + + Scans Dart sources for widget classes (StatefulWidget/StatelessWidget). + If any are found, returns ``"ui_control"``; otherwise ``"service"``. + + Args: + package_path: Path to the extracted Flutter package root. + + Returns: + ``"ui_control"`` or ``"service"``. + """ + lib_dir = package_path / "lib" + if not lib_dir.exists(): + return "service" + + widget_pattern = re.compile( + r"class\s+\w+\s+extends\s+(?:Stateful|Stateless)Widget", + ) + for dart_file in lib_dir.rglob("*.dart"): + content = dart_file.read_text(encoding="utf-8") + if widget_pattern.search(content): + return "ui_control" + + return "service" + + +def parse_dart_package_api( + package_path: Path, + strict: bool = False, + include_widgets: bool = False, +) -> DartPackageAPI: + """Parse a Dart package and return a structured DartPackageAPI. + + Args: + package_path: Path to the extracted Flutter package root. + strict: If True, only include classes with 2+ public methods. + include_widgets: If True, include widget classes and extract their + constructor parameters. Used for ``ui_control`` extensions where + widget constructor params become Flet control properties. + + Returns: + DartPackageAPI with parsed classes, enums, helper classes, and typedefs. + """ + lib_dir = package_path / "lib" + if not lib_dir.exists(): + raise FileNotFoundError(f"Directory {lib_dir} does not exist.") + + all_classes: list[DartClass] = [] + all_enums: list[DartEnum] = [] + all_helpers: list[DartClass] = [] + all_typedefs: dict[str, str] = {} + all_top_level_functions: list[DartMethod] = [] + # Track all parsed class blocks for component class resolution + all_parsed_blocks: dict[str, tuple[str, str]] = {} # class_name → (block, source_file) + + for dart_file in lib_dir.rglob("*.dart"): + content = dart_file.read_text(encoding="utf-8") + relative_path = str(dart_file.relative_to(lib_dir)) + + # Parse typedefs + all_typedefs.update(_parse_typedefs(content)) + + # Parse enums + all_enums.extend(_parse_enums(content)) + + # Parse top-level functions (not inside any class) + all_top_level_functions.extend(_parse_top_level_functions(content, relative_path)) + + # Parse classes + class_matches = re.finditer( + r"(?:@[^\n]*\n)*" + r"(?:abstract\s+)?class\s+(\w+)" + r"(?:\s+extends\s+(\w+))?" + r"(?:\s+with\s+[\w\s,]+)?" + r"(?:\s+implements\s+[\w\s,]+)?" + r"\s*\{", + content, + ) + + for match in class_matches: + class_name = match.group(1) + parent_class = match.group(2) or "" + + # Check for @Deprecated annotation on the class + annotation_text = match.group(0) + if re.search(r"@[Dd]eprecated", annotation_text): + continue + + doc_match = re.search( + r"(///.*?$|/\*\*(.*?)\*/)", + content[: match.start()], + re.DOTALL | re.MULTILINE, + ) + class_doc = _clean_docstring(doc_match) if doc_match else "" + + class_block = _extract_code_block(content[match.end() - 1 :]) + if not class_block: + continue + + # Skip private, deprecated, internal, and platform impl classes early + if ( + class_name.startswith("_") + or "nodoc" in class_doc.lower() + or "internal" in class_doc.lower() + or _is_platform_impl(parent_class) + or parent_class in _INTERNAL_PARENT_BASES + ): + continue + + # Track all viable classes for component resolution + if include_widgets and class_name not in all_parsed_blocks: + all_parsed_blocks[class_name] = (class_block, relative_path) + + # Helper classes (Event, Data, Result, State, etc.) or data model classes + if _is_helper_class(class_name, parent_class): + helper_methods = _parse_class_methods(class_block, skip_filter=False) + all_helpers.append( + DartClass( + name=class_name, + methods=helper_methods, + docstring=class_doc, + parent_class=parent_class, + source_file=relative_path, + ) + ) + continue + + # Widget classes: include when building ui_control extensions + is_widget = parent_class in WIDGET_BASES + if is_widget and include_widgets: + # Skip widgets with internal-looking suffixes. + # Uses a reduced set that intentionally excludes "Widget" — + # many packages name their main user-facing class FooWidget + # (e.g. RiveWidget, VideoWidget). + if class_name.endswith(_INTERNAL_WIDGET_SUFFIXES): + continue + ctor_params = _parse_constructor_params(class_name, class_block) + if ctor_params: + all_classes.append( + DartClass( + name=class_name, + methods=[], + constructor_params=ctor_params, + docstring=class_doc, + parent_class=parent_class, + source_file=relative_path, + ) + ) + continue + + if _should_skip_class(class_name, class_doc, parent_class): + continue + + methods = _parse_class_methods(class_block, skip_filter=True) + + if strict and len(methods) < 2: + continue + + if methods: + all_classes.append( + DartClass( + name=class_name, + methods=methods, + docstring=class_doc, + parent_class=parent_class, + source_file=relative_path, + ) + ) + + # Parse re-exported types from barrel files + reexported = _parse_reexports(lib_dir) + + # Collect component classes (non-widget classes referenced by widget params) + component_classes: list[DartClass] = [] + if include_widgets and all_parsed_blocks: + component_classes = _collect_component_classes( + all_classes, + all_parsed_blocks, + {h.name for h in all_helpers}, + ) + + return DartPackageAPI( + classes=all_classes, + enums=all_enums, + helper_classes=all_helpers, + typedefs=all_typedefs, + reexported_types=reexported, + top_level_functions=all_top_level_functions, + component_classes=component_classes, + ) diff --git a/src/flet_pkg/core/pipeline.py b/src/flet_pkg/core/pipeline.py new file mode 100644 index 0000000..4f34b1c --- /dev/null +++ b/src/flet_pkg/core/pipeline.py @@ -0,0 +1,441 @@ +""" +Generation pipeline orchestrator. + +Coordinates the full flow: download → parse → analyze → generate → write. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +from typing import TYPE_CHECKING + +from flet_pkg.core.analyzer import PackageAnalyzer +from flet_pkg.core.downloader import PubDevDownloader +from flet_pkg.core.generators import ( + DartServiceGenerator, + PythonControlGenerator, + PythonInitGenerator, + PythonSubModuleGenerator, + PythonTypesGenerator, +) +from flet_pkg.core.models import DartPackageAPI, GenerationPlan +from flet_pkg.core.parser import parse_dart_package_api +from flet_pkg.ui.console import console + +if TYPE_CHECKING: + from flet_pkg.core.ai.config import AIConfig + from flet_pkg.core.ai.models import GapReport, RefinementResult + + +@dataclass +class PipelineResult: + """Result of a generation pipeline run.""" + + project_dir: Path + plan: GenerationPlan + files_generated: list[str] = field(default_factory=list) + warnings: list[str] = field(default_factory=list) + coverage_pct: float = 0.0 + gap_report: GapReport | None = None + ai_coverage_pct: float | None = None + + +class GenerationPipeline: + """Orchestrates the full code generation pipeline. + + Steps: + 1. Download Flutter package from pub.dev (or use local path) + 2. Parse Dart sources into DartPackageAPI + 3. Analyze API to produce GenerationPlan + 4. Run generators to produce Python + Dart files + 5. Write generated files to project directory (overwriting template stubs) + """ + + def __init__(self): + """Initialise pipeline with default downloader and analyzer.""" + self.downloader = PubDevDownloader() + self.analyzer = PackageAnalyzer() + + def run( + self, + flutter_package: str, + control_name: str, + extension_type: str, + project_dir: Path, + package_name: str, + description: str = "", + local_package: Path | None = None, + control_name_snake: str = "", + include_console: bool = True, + ai_refine: bool = False, + ai_provider: str | None = None, + ai_model: str | None = None, + verbose: bool = False, + ) -> PipelineResult: + """Run the full generation pipeline. + + Args: + flutter_package: Flutter package name (pub.dev). + control_name: PascalCase control class name. + extension_type: ``"service"`` or ``"ui_control"``. + project_dir: Path to the scaffolded project directory. + package_name: Python package name. + description: Package description. + local_package: If set, use this local path instead of downloading. + control_name_snake: Snake_case name for files (matches template context). + include_console: Whether to include debug console module (default: True). + ai_refine: If True, run AI refinement after generation. + ai_provider: AI provider name (anthropic, openai, google, ollama). + ai_model: AI model name override. + verbose: If True, print detailed progress at each step. + + Returns: + PipelineResult with generated file list and warnings. + """ + result = PipelineResult( + project_dir=project_dir, + plan=GenerationPlan( + control_name=control_name, + package_name=package_name, + ), + ) + + # Step 1: Get package source + try: + if local_package: + package_path = local_package + console.print(f" [info]Using local package:[/info] {package_path}") + else: + package_path = self.downloader.download(flutter_package) + console.print(f" [success]Downloaded {flutter_package}[/success]") + except Exception as e: + result.warnings.append(f"Download failed: {e}") + console.print(f" [warning]Download failed: {e}[/warning]") + return result + + # Step 2: Parse + try: + api = parse_dart_package_api( + package_path, + include_widgets=(extension_type == "ui_control"), + ) + n_classes = len(api.classes) + n_enums = len(api.enums) + n_helpers = len(api.helper_classes) + n_funcs = len(api.top_level_functions) + parts = [f"{n_classes} classes", f"{n_enums} enums", f"{n_helpers} helper types"] + if n_funcs: + parts.append(f"{n_funcs} top-level functions") + console.print(f" [success]Parsed {', '.join(parts)}[/success]") + if verbose: + for cls in api.classes: + console.print(f" [dim]class {cls.name}[/dim]") + for enum in api.enums: + console.print(f" [dim]enum {enum.name}[/dim]") + except Exception as e: + result.warnings.append(f"Parse failed: {e}") + console.print(f" [warning]Parse failed: {e}[/warning]") + return result + + if not api.classes and not api.top_level_functions: + result.warnings.append("No public classes or functions found in package.") + console.print(" [warning]No public classes or functions found.[/warning]") + return result + + # Step 3: Analyze + try: + plan = self.analyzer.analyze( + api, + control_name=control_name, + extension_type=extension_type, + flutter_package=flutter_package, + package_name=package_name, + description=description, + ) + # Set control_name_snake from the template context + plan.control_name_snake = control_name_snake + plan.include_console = include_console + result.plan = plan + + # Resolve re-exported types from platform_interface packages + if api.reexported_types: + try: + self.analyzer.resolve_platform_types(api, plan) + except Exception as e: + result.warnings.append(f"Platform type resolution failed: {e}") + + n_methods = len(plan.main_methods) + sum(len(s.methods) for s in plan.sub_modules) + console.print( + f" [success]Analyzed: {n_methods} methods, " + f"{len(plan.sub_modules)} sub-modules, " + f"{len(plan.events)} events[/success]" + ) + if plan.sub_controls: + n_sub = len(plan.sub_controls) + names = ", ".join(sc.control_name for sc in plan.sub_controls) + console.print(f" [success]Detected {n_sub} sub-control(s): {names}[/success]") + if plan.widget_family_variants: + n_var = len(plan.widget_family_variants) + console.print(f" [success]Widget family: {n_var} variants[/success]") + if plan.sibling_widgets: + sib_names = ", ".join(s.control_name for s in plan.sibling_widgets) + console.print(f" [success]Sibling widgets: {sib_names}[/success]") + if verbose: + for m in plan.main_methods: + console.print(f" [dim]method: {m.python_name}()[/dim]") + for sub in plan.sub_modules: + console.print(f" [dim]sub-module: {sub.module_name}[/dim]") + for m in sub.methods: + console.print(f" [dim]{m.python_name}()[/dim]") + for ev in plan.events: + console.print(f" [dim]event: {ev.python_attr_name}[/dim]") + for p in getattr(plan, "properties", []): + console.print(f" [dim]property: {p.python_name}[/dim]") + except Exception as e: + result.warnings.append(f"Analysis failed: {e}") + console.print(f" [warning]Analysis failed: {e}[/warning]") + return result + + # Step 4: Generate + generators = [ + PythonControlGenerator(), + PythonSubModuleGenerator(), + PythonTypesGenerator(), + PythonInitGenerator(), + DartServiceGenerator(), + ] + + all_files: dict[str, str] = {} + for gen in generators: + try: + files = gen.generate(plan) + all_files.update(files) + except Exception as e: + result.warnings.append(f"Generator {gen.__class__.__name__} failed: {e}") + + if verbose: + py_files = [f for f in all_files if f.endswith(".py")] + dart_files = [f for f in all_files if f.endswith(".dart")] + console.print( + f" [dim]{len(py_files)} Python files, {len(dart_files)} Dart files[/dim]" + ) + for fname in sorted(all_files): + console.print(f" [dim]{fname}[/dim]") + + # Step 4.1: Gap Analysis (deterministic — no AI) + try: + gap_report = self._run_gap_analysis(api, plan, extension_type) + result.coverage_pct = gap_report.coverage_pct + result.gap_report = gap_report + except Exception as e: + result.warnings.append(f"Gap analysis failed: {e}") + + # Step 4.5: AI Refinement (optional) + if ai_refine: + try: + from flet_pkg.core.ai.config import AIConfig + from flet_pkg.core.ai.refiner import AIRefiner + + config = AIConfig.load(provider=ai_provider, model=ai_model) + if config.is_available(): + label = "Running AI refinement..." + if verbose: + label += f" [dim]({config.provider} / {config.model})[/dim]" + console.print(f" [info]{label}[/info]") + refiner = AIRefiner(config) + ai_result = refiner.refine( + api, + plan, + all_files, + extension_type, + package_path, + verbose=verbose, + ) + if ai_result.edits_applied > 0: + console.print( + f" [success]AI refined: {ai_result.edits_applied} edits applied" + f"[/success]" + ) + if not ai_result.validation_passed and ai_result.edits_applied > 0: + result.warnings.append("AI edits failed validation — using originals") + # Estimate post-AI coverage from applied edits + if ai_result.validation_passed and ai_result.edits_applied > 0: + gap = ai_result.gap_report + if gap.total_dart_api > 0: + new_mapped = gap.total_generated + ai_result.edits_applied + result.ai_coverage_pct = min( + new_mapped / gap.total_dart_api * 100, 100.0 + ) + # Always write .ai-review.md when AI refinement runs + _write_ai_review(project_dir, config, ai_result) + else: + console.print(" [warning]AI skipped: no API key configured[/warning]") + except ImportError: + console.print(" [warning]AI skipped: install with uv add flet-pkg[ai][/warning]") + except Exception as e: + import logging + + logging.debug("AI refinement error: %s", e, exc_info=True) + result.warnings.append(_format_ai_error(e)) + + # Step 5: Write files (overwriting template stubs) + python_pkg_dir = project_dir / "src" / package_name + dart_src_dir = project_dir / "src" / "flutter" / package_name / "lib" / "src" + + dart_lib_dir = project_dir / "src" / "flutter" / package_name / "lib" + + for filename, content in all_files.items(): + if filename == "extension.dart": + # extension.dart lives at lib/extension.dart (not lib/src/) + target = dart_lib_dir / filename + elif filename.endswith(".dart"): + target = dart_src_dir / filename + else: + target = python_pkg_dir / filename + + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(content, encoding="utf-8") + result.files_generated.append(str(target.relative_to(project_dir))) + + # Clean up template stubs that were replaced by generated files + self._cleanup_stubs(project_dir, package_name, all_files, result) + + console.print(f" [success]Generated {len(result.files_generated)} files[/success]") + + return result + + @staticmethod + def _run_gap_analysis( + api: DartPackageAPI, + plan: GenerationPlan, + extension_type: str, + ) -> GapReport: + """Run deterministic gap analysis (no AI/LLM).""" + from flet_pkg.core.ai.gap_analyzer import GapAnalyzer + + analyzer = GapAnalyzer() + return analyzer.analyze(api, plan, extension_type) + + def _cleanup_stubs( + self, + project_dir: Path, + package_name: str, + generated_files: dict[str, str], + result: PipelineResult, + ) -> None: + """Remove template stub files that conflict with generated files. + + The template creates stub files like ``onesignal.py`` and + ``onesignal_service.dart``, but the generators may produce files + with the same purpose under different names. This method removes + orphaned stubs to prevent import conflicts. + """ + python_pkg_dir = project_dir / "src" / package_name + dart_src_dir = project_dir / "src" / "flutter" / package_name / "lib" / "src" + + generated_py = {f for f in generated_files if f.endswith(".py")} + generated_dart = {f for f in generated_files if f.endswith(".dart")} + + # Check for conflicting Python stubs + for py_file in python_pkg_dir.glob("*.py"): + name = py_file.name + if name in generated_py or name == "__init__.py": + continue + # Check if this is a stub with TODO content (from template) + try: + content = py_file.read_text(encoding="utf-8") + if "TODO" in content or "example_method" in content: + py_file.unlink() + except Exception: + pass + + # Check for conflicting Dart stubs + # Don't protect extension.dart if we generated our own + protect_extension = "extension.dart" not in generated_files + for dart_file in dart_src_dir.glob("*.dart"): + name = dart_file.name + if name in generated_dart or (name == "extension.dart" and protect_extension): + continue + try: + content = dart_file.read_text(encoding="utf-8") + if "TODO" in content or "example_method" in content: + dart_file.unlink() + except Exception: + pass + + +def _write_ai_review( + project_dir: Path, + config: AIConfig, + ai_result: RefinementResult, +) -> None: + """Write .ai-review.md with diffs and pending suggestions.""" + lines: list[str] = [] + lines.append("# AI Review Report\n") + + # Header + lines.append(f"- **Provider:** {config.provider}") + lines.append(f"- **Model:** {config.model}") + lines.append(f"- **Coverage:** {ai_result.gap_report.coverage_pct:.1f}%") + lines.append(f"- **Edits applied:** {ai_result.edits_applied}") + lines.append(f"- **Edits failed:** {ai_result.edits_failed}") + status = "passed" if ai_result.validation_passed else "FAILED" + lines.append(f"- **Validation:** {status}") + lines.append("") + + # Diffs section + if ai_result.file_diffs: + lines.append("## Diffs\n") + for filename, diff_text in ai_result.file_diffs: + lines.append(f"### {filename}\n") + lines.append("```diff") + lines.append(diff_text.rstrip()) + lines.append("```\n") + else: + lines.append("## Diffs\n") + lines.append("_No files were modified._\n") + + # Pending suggestions + if ai_result.pending_suggestions: + lines.append("## Suggestions (not applied)\n") + for s in ai_result.pending_suggestions: + lines.append(f"- **[P{s.priority}] {s.target_file}:** {s.description}") + lines.append("") + + review_path = project_dir / ".ai-review.md" + review_path.write_text("\n".join(lines), encoding="utf-8") + console.print(" [info]Review saved: .ai-review.md[/info]") + + +def _format_ai_error(exc: Exception) -> str: + """Format AI provider errors into user-friendly messages.""" + msg = str(exc) + lower = msg.lower() + + if "insufficient_quota" in msg or "billing" in lower: + return "AI skipped: API quota exceeded. Check your billing at your provider's dashboard." + if "401" in msg or "invalid_api_key" in msg or "Unauthorized" in msg: + return "AI skipped: invalid API key. Verify your API key environment variable is correct." + if "rate_limit" in msg or "429" in msg: + return "AI skipped: rate limit reached. Try again later." + if "timeout" in lower or "timed out" in lower: + return "AI skipped: request timed out. Try again later." + if "context length" in lower or "too long" in lower: + return ( + "AI skipped: input too long for the model's context window. " + "Try a model with a larger context or reduce the package size." + ) + if "maximum retries" in lower or "output validation" in lower: + return ( + "AI skipped: model failed to produce valid structured output. " + "Try a larger model (e.g. --ai-model qwen2.5-coder:32b) " + "or use a cloud provider (--ai-provider anthropic)." + ) + if "500" in msg or "server_error" in lower or "internal server error" in lower: + return ( + "AI skipped: provider returned a server error (500). " + "This is a transient issue on the provider's side. Try again later." + ) + + return f"AI refinement failed: {msg}" diff --git a/src/flet_pkg/core/prompts.py b/src/flet_pkg/core/prompts.py new file mode 100644 index 0000000..1f71a8f --- /dev/null +++ b/src/flet_pkg/core/prompts.py @@ -0,0 +1,52 @@ +from typing import Callable + +from rich.prompt import Confirm, Prompt + +from flet_pkg.ui.console import console + + +def ask( + prompt: str, + default: str = "", + validator: Callable[[str], str | None] | None = None, +) -> str: + while True: + value = Prompt.ask(f"[info]?[/info] {prompt}", default=default or None, console=console) + if value is None: + value = "" + value = value.strip() + if validator: + error = validator(value) + if error: + console.print(f" [error]{error}[/error]") + continue + if not value and not default: + console.print(" [error]This field is required.[/error]") + continue + return value + + +def ask_confirm(prompt: str, default: bool = True) -> bool: + """Ask a yes/no confirmation question. + + Returns ``True`` for yes, ``False`` for no. + """ + return Confirm.ask(f"[info]?[/info] {prompt}", default=default, console=console) + + +def ask_choice(prompt: str, choices: list[tuple[int, str]]) -> int: + console.print(f"\n[info]?[/info] {prompt}") + for num, label in choices: + console.print(f" [highlight]{num}[/highlight] - {label}") + while True: + value = Prompt.ask(" Choice", console=console) + try: + choice = int(value) + valid = [c[0] for c in choices] + if choice in valid: + return choice + except ValueError: + pass + console.print( + f" [error]Please enter one of: {', '.join(str(c[0]) for c in choices)}[/error]" + ) diff --git a/src/flet_pkg/core/registry_checker.py b/src/flet_pkg/core/registry_checker.py new file mode 100644 index 0000000..5c02b6a --- /dev/null +++ b/src/flet_pkg/core/registry_checker.py @@ -0,0 +1,118 @@ +""" +Registry checker for PyPI and GitHub. + +Checks whether a package name already exists on PyPI or has +matching repositories on GitHub, so the user can be warned +before scaffolding a conflicting project name. +""" + +from dataclasses import dataclass + +import httpx + +_TIMEOUT = 10 + + +@dataclass +class RegistryMatch: + """A match found on a package registry or code host.""" + + source: str + name: str + url: str + description: str = "" + + +def check_pypi(name: str) -> RegistryMatch | None: + """Check if *name* exists as a PyPI package. + + Returns a ``RegistryMatch`` when the package exists, ``None`` when it + does not (404), or ``None`` on any network/timeout error. + """ + url = f"https://pypi.org/pypi/{name}/json" + try: + response = httpx.get(url, follow_redirects=True, timeout=_TIMEOUT) + except httpx.HTTPError: + return None + + if response.status_code != 200: + return None + + data = response.json() + info = data.get("info", {}) + return RegistryMatch( + source="PyPI", + name=info.get("name", name), + url=info.get("project_url", f"https://pypi.org/project/{name}/"), + description=info.get("summary", ""), + ) + + +_FLET_PACKAGES_URL = "https://github.com/flet-dev/flet/tree/main/sdk/python/packages/{name}" +_FLET_PACKAGES_API = ( + "https://api.github.com/repos/flet-dev/flet/contents/sdk/python/packages/{name}" +) + + +def check_flet_packages(name: str) -> RegistryMatch | None: + """Check if *name* exists as an official Flet extension package. + + Queries the ``flet-dev/flet`` monorepo at + ``sdk/python/packages/``. Returns a ``RegistryMatch`` when + the directory exists, ``None`` otherwise (including network errors). + """ + api_url = _FLET_PACKAGES_API.format(name=name) + try: + response = httpx.get(api_url, follow_redirects=True, timeout=_TIMEOUT) + except httpx.HTTPError: + return None + + if response.status_code != 200: + return None + + return RegistryMatch( + source="Flet SDK", + name=name, + url=_FLET_PACKAGES_URL.format(name=name), + description="Official Flet extension in flet-dev/flet monorepo", + ) + + +def check_github(query: str, max_results: int = 3) -> list[RegistryMatch]: + """Search GitHub repositories whose name matches *query*. + + Uses ``in:name`` to restrict the search to repository names only, + avoiding false positives from description/README mentions. + + Returns up to *max_results* matches, or an empty list on any + network/timeout error. + """ + url = "https://api.github.com/search/repositories" + params = {"q": f"{query} in:name", "per_page": max_results} + try: + response = httpx.get(url, params=params, follow_redirects=True, timeout=_TIMEOUT) + except httpx.HTTPError: + return [] + + if response.status_code != 200: + return [] + + data = response.json() + query_lower = query.lower() + results: list[RegistryMatch] = [] + for item in data.get("items", [])[:max_results]: + # full_name is "owner/repo-name" — check repo part + full_name = item.get("full_name", "").lower() + repo_name = full_name.rsplit("/", maxsplit=1)[-1] if "/" in full_name else full_name + # Only include repos whose name contains the full query + if query_lower not in repo_name: + continue + results.append( + RegistryMatch( + source="GitHub", + name=item.get("full_name", ""), + url=item.get("html_url", ""), + description=item.get("description", "") or "", + ) + ) + return results diff --git a/src/flet_pkg/core/scaffolder.py b/src/flet_pkg/core/scaffolder.py new file mode 100644 index 0000000..68e3e2c --- /dev/null +++ b/src/flet_pkg/core/scaffolder.py @@ -0,0 +1,106 @@ +"""Jinja2-based project scaffolder. + +Walks a template directory tree, resolves ``{{variable}}`` placeholders +in file and directory names, and renders ``.jinja`` files through +Jinja2 to produce a complete project skeleton. +""" + +import os +import re +import shutil +from pathlib import Path + +from jinja2 import Environment, FileSystemLoader, StrictUndefined + +from flet_pkg.ui.console import console + +TEMPLATE_DIR = Path(__file__).parent.parent / "templates" + +VARIABLE_RE = re.compile(r"\{\{(\w+)\}\}") + + +class Scaffolder: + """Render a project from a template directory. + + Attributes: + template_path: Resolved path to the template directory. + context: Variable mapping used for name resolution and rendering. + output_dir: Root directory where the project will be created. + env: Jinja2 environment configured with strict undefined. + """ + + def __init__(self, template_name: str, context: dict, output_dir: Path | None = None): + """Initialise the scaffolder. + + Args: + template_name: Name of the template (``service`` or ``ui_control``). + context: Dictionary of variables for Jinja2 rendering. + output_dir: Target directory. Defaults to the current working directory. + + Raises: + FileNotFoundError: If the template directory does not exist. + """ + self.template_path = TEMPLATE_DIR / template_name + if not self.template_path.is_dir(): + raise FileNotFoundError(f"Template '{template_name}' not found at {self.template_path}") + + self.context = context + self.output_dir = output_dir or Path.cwd() + + self.env = Environment( + loader=FileSystemLoader(str(self.template_path)), + undefined=StrictUndefined, + keep_trailing_newline=True, + ) + + def _resolve_name(self, name: str) -> str: + """Replace ``{{variable}}`` placeholders in *name* using the context.""" + return VARIABLE_RE.sub(lambda m: str(self.context.get(m.group(1), m.group(0))), name) + + def generate(self) -> Path: + """Generate the project directory from the template. + + Returns: + Path to the created project directory. + + Raises: + FileExistsError: If the project directory already exists. + """ + project_name = self.context.get("project_name", "output") + project_dir = self.output_dir / project_name + if project_dir.exists(): + raise FileExistsError(f"Directory already exists: {project_dir}") + + with console.status("[info]Generating project files...[/info]", spinner="dots"): + self._walk_and_render(self.output_dir) + + return project_dir + + def _walk_and_render(self, project_dir: Path) -> None: + """Recursively walk the template tree and render files into *project_dir*.""" + for dirpath, dirnames, filenames in os.walk(self.template_path): + rel_dir = Path(dirpath).relative_to(self.template_path) + resolved_dir = ( + Path(*[self._resolve_name(p) for p in rel_dir.parts]) if rel_dir.parts else Path() + ) + target_dir = project_dir / resolved_dir + target_dir.mkdir(parents=True, exist_ok=True) + + # Skip hidden directories + dirnames[:] = [d for d in dirnames if not d.startswith(".")] + + for filename in filenames: + if filename == "template.yaml": + continue + + src_file = Path(dirpath) / filename + resolved_name = self._resolve_name(filename) + + if resolved_name.endswith(".jinja"): + resolved_name = resolved_name[: -len(".jinja")] + rel_template = str(Path(dirpath).relative_to(self.template_path) / filename) + template = self.env.get_template(rel_template.replace(os.sep, "/")) + content = template.render(self.context) + (target_dir / resolved_name).write_text(content, encoding="utf-8") + else: + shutil.copy2(src_file, target_dir / resolved_name) diff --git a/src/flet_pkg/core/type_map.py b/src/flet_pkg/core/type_map.py new file mode 100644 index 0000000..30bcc1c --- /dev/null +++ b/src/flet_pkg/core/type_map.py @@ -0,0 +1,317 @@ +""" +Dart-to-Python type mapping. + +Converts Dart type annotations to their Python equivalents, +handling nullable types, generics, and common Flutter types. + +Also provides Flet-aware mappings for UI controls that use native +Flet types (``ft.Alignment``, ``ft.BoxFit``, etc.) and typed Dart +getters (``getAlignment``, ``getBoxFit``, etc.). +""" + +import re + +# Direct type mappings: Dart type -> Python type +_TYPE_MAP: dict[str, str] = { + "String": "str", + "bool": "bool", + "int": "int", + "double": "float", + "num": "float", + "void": "None", + "dynamic": "Any", + "Object": "Any", + "Color": "str", + "Duration": "int", + "DateTime": "str", + "Uint8List": "bytes", + "BigInt": "int", + "Uri": "str", + "Rect": "dict", + # Common Flutter enums (mapped to str for Flet serialization) + "TextDirection": "str", + "Axis": "str", + "StackFit": "str", + "MainAxisAlignment": "str", + "CrossAxisAlignment": "str", + "MainAxisSize": "str", + "VerticalDirection": "str", + "TextAlign": "str", + "TextOverflow": "str", + "FontWeight": "str", + "FontStyle": "str", + "TextBaseline": "str", + "Clip": "str", + "BoxFit": "str", + "Alignment": "str", + "AlignmentGeometry": "str", + "AlignmentDirectional": "str", + "BlendMode": "str", + "FilterQuality": "str", + "ImageRepeat": "str", + "Curve": "str", + "StrokeCap": "str", + "StrokeJoin": "str", + "PaintingStyle": "str", + "TileMode": "str", + "WrapAlignment": "str", + "WrapCrossAlignment": "str", + "HitTestBehavior": "str", + "DragStartBehavior": "str", + "ScrollViewKeyboardDismissBehavior": "str", + "TextInputType": "str", + "TextInputAction": "str", + "TextCapitalization": "str", + "Brightness": "str", + "TabBarIndicatorSize": "str", + "FloatingLabelBehavior": "str", +} + +# Flet-aware type mappings: Dart type -> Python type using native Flet types. +# Used by ``map_dart_type_flet()`` for UI control extensions. +_FLET_TYPE_MAP: dict[str, str | None] = { + "Alignment": "ft.Alignment", + "AlignmentGeometry": "ft.Alignment", + "AlignmentDirectional": "ft.Alignment", + "BoxFit": "ft.BoxFit", + "Fit": "ft.BoxFit", # rive package uses Fit instead of BoxFit + "Rect": "ft.Rect", + "Color": "ft.Color", + "double": "ft.Number", + "num": "ft.Number", + "Widget": "ft.Control", + "TextStyle": "ft.TextStyle", + "Key": None, # skip — not a user-facing property +} + +# Mapping from Python type -> Dart getter expression. +# ``{name}`` is replaced with the actual property name at generation time. +_FLET_DART_GETTER_MAP: dict[str, str] = { + "ft.Alignment": 'control.getAlignment("{name}")', + "ft.BoxFit": 'control.getBoxFit("{name}")', + "ft.Rect": 'control.getRect("{name}")', + "ft.Color": 'control.getString("{name}")', + "ft.Number": 'control.getDouble("{name}")', + "ft.Control": 'buildWidget("{name}")', + "ft.TextStyle": 'control.getTextStyle("{name}", Theme.of(context))', + "bool": 'control.getBool("{name}", false)!', + "int": 'control.getInt("{name}")', + "float": 'control.getDouble("{name}")', + "str": 'control.getString("{name}")', +} + +# Regex to extract generic type parameters: e.g. List -> ("List", "String") +_GENERIC_RE = re.compile(r"^(\w+)\s*<(.+)>$") + +# Regex to detect nullable: e.g. String? -> ("String", True) +_NULLABLE_RE = re.compile(r"^(.+?)\?$") + + +def map_dart_type(dart_type: str) -> str: + """Map a Dart type annotation to its Python equivalent. + + Handles nullable types (``String?`` -> ``str | None``), generics + (``List`` -> ``list[str]``), nested generics + (``Map>`` -> ``dict[str, list[int]]``), and + ``Future`` (unwraps to the inner type). + + Args: + dart_type: The Dart type string to convert. + + Returns: + The corresponding Python type string. + """ + dart_type = dart_type.strip() + if not dart_type: + return "Any" + + # Handle nullable + nullable_match = _NULLABLE_RE.match(dart_type) + if nullable_match: + inner = map_dart_type(nullable_match.group(1)) + return f"{inner} | None" + + # Handle generics + generic_match = _GENERIC_RE.match(dart_type) + if generic_match: + outer = generic_match.group(1) + inner_raw = generic_match.group(2) + + # Future -> unwrap to T + if outer == "Future": + return map_dart_type(inner_raw) + + # Split generic params respecting nested angle brackets + inner_types = _split_generic_params(inner_raw) + mapped_inner = ", ".join(map_dart_type(t) for t in inner_types) + + if outer == "List": + return f"list[{mapped_inner}]" + if outer == "Set": + return f"set[{mapped_inner}]" + if outer == "Map": + return f"dict[{mapped_inner}]" + if outer == "Iterable": + return f"list[{mapped_inner}]" + + # Unknown generic: just map the outer + mapped_outer = _TYPE_MAP.get(outer, outer) + return f"{mapped_outer}[{mapped_inner}]" + + # Direct lookup + if dart_type in _TYPE_MAP: + return _TYPE_MAP[dart_type] + + # Function types + if "Function" in dart_type: + return "Any" + + # Unknown type: pass through as-is + return dart_type + + +def map_return_type(dart_type: str) -> tuple[str, bool]: + """Map a Dart return type, detecting if the method is async. + + Args: + dart_type: The Dart return type string. + + Returns: + A tuple of (python_type, is_async). ``is_async`` is True + when the Dart return type is ``Future``. + """ + dart_type = dart_type.strip() + if not dart_type: + return "None", False + + generic_match = _GENERIC_RE.match(dart_type) + if generic_match and generic_match.group(1) == "Future": + inner = generic_match.group(2).strip() + if inner == "void": + return "None", True + return map_dart_type(inner), True + + if dart_type == "Future": + return "None", True + + return map_dart_type(dart_type), False + + +def _split_generic_params(params_str: str) -> list[str]: + """Split generic type parameters respecting nested angle brackets. + + ``"String, List"`` -> ``["String", "List"]`` + """ + parts: list[str] = [] + current: list[str] = [] + depth = 0 + for char in params_str: + if char == "<": + depth += 1 + current.append(char) + elif char == ">": + depth -= 1 + current.append(char) + elif char == "," and depth == 0: + parts.append("".join(current).strip()) + current = [] + else: + current.append(char) + if current: + parts.append("".join(current).strip()) + return parts + + +# ------------------------------------------------------------------ +# Flet-aware mapping (for UI controls) +# ------------------------------------------------------------------ + + +def map_dart_type_flet(dart_type: str) -> str | None: + """Map a Dart type to its Flet-aware Python equivalent. + + Consults ``_FLET_TYPE_MAP`` first for native Flet types (e.g. + ``Alignment`` → ``ft.Alignment``), then falls back to + ``map_dart_type()``. + + Returns ``None`` when the type should be skipped entirely (e.g. + ``Key``). + """ + dart_type = dart_type.strip() + if not dart_type: + return "Any" + + # Handle nullable + nullable_match = _NULLABLE_RE.match(dart_type) + if nullable_match: + inner = map_dart_type_flet(nullable_match.group(1)) + if inner is None: + return None + return f"{inner} | None" + + # Handle generics + generic_match = _GENERIC_RE.match(dart_type) + if generic_match: + outer = generic_match.group(1) + inner_raw = generic_match.group(2) + + # Future → unwrap to T + if outer == "Future": + return map_dart_type_flet(inner_raw) + + inner_types = _split_generic_params(inner_raw) + mapped = [] + for t in inner_types: + m = map_dart_type_flet(t) + if m is None: + return None + mapped.append(m) + mapped_inner = ", ".join(mapped) + + if outer == "List": + return f"list[{mapped_inner}]" + if outer == "Set": + return f"set[{mapped_inner}]" + if outer == "Map": + return f"dict[{mapped_inner}]" + if outer == "Iterable": + return f"list[{mapped_inner}]" + + mapped_outer = _FLET_TYPE_MAP.get(outer) + if mapped_outer is None and outer in _FLET_TYPE_MAP: + return None # explicitly skipped + if mapped_outer is None: + mapped_outer = _TYPE_MAP.get(outer, outer) + return f"{mapped_outer}[{mapped_inner}]" + + # Flet type lookup (may return None for skipped types) + if dart_type in _FLET_TYPE_MAP: + return _FLET_TYPE_MAP[dart_type] + + # Fallback to standard mapping + return map_dart_type(dart_type) + + +def get_flet_dart_getter(python_type: str, prop_name: str) -> str: + """Return the Dart getter expression for a Flet property. + + Args: + python_type: The mapped Python type (e.g. ``"ft.Alignment"``). + prop_name: The property name (e.g. ``"alignment"``). + + Returns: + A Dart expression string like ``control.getAlignment("alignment")``. + Falls back to ``control.getString("{prop_name}")`` for unknown types. + """ + # Strip nullable and list wrappers for lookup + base = python_type.replace(" | None", "").strip() + + # Handle list types: list[ft.Control] → buildWidgets("{name}") + if base.startswith("list["): + inner = base[5:-1] + if inner == "ft.Control": + return f'buildWidgets("{prop_name}")' + return f'control.getString("{prop_name}")' + + template = _FLET_DART_GETTER_MAP.get(base, 'control.getString("{name}")') + return template.replace("{name}", prop_name) diff --git a/src/flet_pkg/core/validators.py b/src/flet_pkg/core/validators.py new file mode 100644 index 0000000..bc27302 --- /dev/null +++ b/src/flet_pkg/core/validators.py @@ -0,0 +1,123 @@ +"""Name validation and derivation utilities. + +Provides validators for Flutter package names, Python package names, +project names, and control class names. Also derives a consistent set +of names from a Flutter package name. +""" + +import re +from dataclasses import dataclass + + +@dataclass +class DerivedNames: + """Set of names derived from a Flutter package name. + + Attributes: + project_name: Hyphenated project name (e.g. ``flet-onesignal``). + package_name: Python package name (e.g. ``flet_onesignal``). + control_name: PascalCase control class name (e.g. ``Onesignal``). + control_name_snake: Snake-case control name (e.g. ``onesignal``). + """ + + project_name: str + package_name: str + control_name: str + control_name_snake: str + + +def validate_package_name(name: str) -> str | None: + """Validate a Python package name. + + Args: + name: Package name to validate. + + Returns: + Error message string, or ``None`` if valid. + """ + if not re.fullmatch(r"[a-z][a-z0-9_]*", name): + return "Must be a valid Python identifier: lowercase, underscores, starts with letter." + return None + + +def validate_project_name(name: str) -> str | None: + """Validate a project name (lowercase + hyphens). + + Args: + name: Project name to validate. + + Returns: + Error message string, or ``None`` if valid. + """ + if not re.fullmatch(r"[a-z][a-z0-9\-]*", name): + return "Must be lowercase letters, digits, and hyphens. Starts with letter." + return None + + +def validate_flutter_package(name: str) -> str | None: + """Validate a pub.dev package name. + + Args: + name: Flutter package name to validate. + + Returns: + Error message string, or ``None`` if valid. + """ + if not re.fullmatch(r"[a-z][a-z0-9_]*", name): + return "Must be a valid pub.dev package name: lowercase, underscores, starts with letter." + return None + + +def validate_control_name(name: str) -> str | None: + """Validate a PascalCase control class name. + + Args: + name: Control class name to validate. + + Returns: + Error message string, or ``None`` if valid. + """ + if not re.fullmatch(r"[A-Z][a-zA-Z0-9]*", name): + return "Must be PascalCase: starts with uppercase letter, alphanumeric." + return None + + +def _camel_to_snake(name: str) -> str: + """Convert a PascalCase or camelCase string to snake_case.""" + return re.sub(r"(? str: + """Remove common Flutter suffixes/prefixes (``_flutter``, ``flutter_``, etc.).""" + for suffix in ("_flutter", "_plus", "_pro"): + if name.endswith(suffix): + return name[: -len(suffix)] + for prefix in ("flutter_",): + if name.startswith(prefix): + return name[len(prefix) :] + return name + + +def _derive_control_name(core: str) -> str: + """Convert a snake_case core name to PascalCase.""" + parts = core.split("_") + return "".join(p.capitalize() for p in parts) + + +def derive_names(flutter_package: str) -> DerivedNames: + """Derive project, package, and control names from a Flutter package name. + + Args: + flutter_package: A pub.dev package name (e.g. ``onesignal_flutter``). + + Returns: + A ``DerivedNames`` instance with all derived names. + """ + core = _strip_flutter_affixes(flutter_package) + control_name = _derive_control_name(core) + return DerivedNames( + project_name=f"flet-{core.replace('_', '-')}", + package_name=f"flet_{core}", + control_name=control_name, + control_name_snake=_camel_to_snake(control_name).lower(), + ) diff --git a/src/flet_pkg/main.py b/src/flet_pkg/main.py new file mode 100644 index 0000000..04abaf1 --- /dev/null +++ b/src/flet_pkg/main.py @@ -0,0 +1,66 @@ +"""Typer CLI application for flet-pkg. + +Registers the ``create`` command and provides a ``--version`` callback. +""" + +import typer + +from flet_pkg import __app_name__, __version__ +from flet_pkg.commands.create import create + +app = typer.Typer( + name=__app_name__, + help=( + "[bold]Scaffold Flet extension packages from Flutter packages.[/bold]\n\n" + "flet-pkg downloads a Flutter package from [link=https://pub.dev]pub.dev[/link], " + "parses its Dart API, and generates a complete Flet extension with " + "Python controls, type mappings, event handlers, and Dart bridge code.\n\n" + "[dim]Extension types:[/dim]\n\n" + " [cyan]service[/cyan] Non-visual extensions " + "(e.g. shared_preferences, geolocator)\n" + " [cyan]ui_control[/cyan] Visual widget extensions " + "(e.g. shimmer, flutter_rating_bar)\n\n" + "[dim]AI refinement (optional):[/dim]\n\n" + " Uses the Architect/Editor pattern to analyze coverage gaps and\n" + " auto-improve the generated code with LLM-powered edits.\n\n" + " [green]$[/green] uv add flet-pkg\\[ai] [dim]# install AI deps[/dim]\n" + " [green]$[/green] ollama pull qwen2.5-coder:14b [dim]# free, local[/dim]\n" + " [green]$[/green] flet-pkg create --ai-refine [dim]# uses Ollama by default[/dim]\n\n" + " Providers: [cyan]ollama[/cyan] (default, free), [cyan]anthropic[/cyan], " + "[cyan]openai[/cyan], [cyan]google[/cyan]\n\n" + "[dim]Quick start:[/dim]\n\n" + " [green]$[/green] flet-pkg create\n" + " [green]$[/green] flet-pkg create -t service -f shared_preferences\n" + " [green]$[/green] flet-pkg create -t ui_control -f shimmer --ai-refine" + ), + rich_markup_mode="rich", + no_args_is_help=True, + add_completion=False, + epilog=( + "[dim]Docs :[/dim] https://brunobrown.github.io/flet-pkg\n\n" + "[dim]Issues:[/dim] https://github.com/brunobrown/flet-pkg/issues" + ), +) + +app.command()(create) + + +def _version_callback(value: bool) -> None: + """Print the version string and exit when ``--version`` is passed.""" + if value: + typer.echo(f"{__app_name__} {__version__}") + raise typer.Exit() + + +@app.callback(invoke_without_command=True) +def main( + version: bool = typer.Option( + False, + "--version", + help="Show version and exit.", + callback=_version_callback, + is_eager=True, + ), +) -> None: + """Root callback invoked before any sub-command.""" + pass diff --git a/src/flet_pkg/mcp/__init__.py b/src/flet_pkg/mcp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/flet_pkg/mcp/_serializers.py b/src/flet_pkg/mcp/_serializers.py new file mode 100644 index 0000000..4006ac4 --- /dev/null +++ b/src/flet_pkg/mcp/_serializers.py @@ -0,0 +1,22 @@ +"""Recursive dataclass-to-JSON-safe dict converter.""" + +import dataclasses +from enum import Enum +from pathlib import Path + + +def to_dict(obj: object) -> object: + """Recursively convert dataclass/Path/Enum/tuple to JSON-safe dict.""" + if dataclasses.is_dataclass(obj) and not isinstance(obj, type): + return {f.name: to_dict(getattr(obj, f.name)) for f in dataclasses.fields(obj)} + if isinstance(obj, Path): + return str(obj) + if isinstance(obj, Enum): + return obj.value + if isinstance(obj, tuple): + return [to_dict(item) for item in obj] + if isinstance(obj, list): + return [to_dict(item) for item in obj] + if isinstance(obj, dict): + return {k: to_dict(v) for k, v in obj.items()} + return obj diff --git a/src/flet_pkg/mcp/server.py b/src/flet_pkg/mcp/server.py new file mode 100644 index 0000000..ddc41bf --- /dev/null +++ b/src/flet_pkg/mcp/server.py @@ -0,0 +1,424 @@ +"""MCP server for flet-pkg. + +Exposes scaffolding and code generation capabilities to AI agents +via the Model Context Protocol (tools, resources, prompts). +""" + +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from dataclasses import dataclass +from pathlib import Path + +from mcp.server.fastmcp import Context, FastMCP +from mcp.server.session import ServerSession + +from flet_pkg.core.downloader import PubDevDownloader +from flet_pkg.core.scaffolder import TEMPLATE_DIR +from flet_pkg.core.type_map import ( + _FLET_TYPE_MAP, + _TYPE_MAP, + map_dart_type, + map_dart_type_flet, +) +from flet_pkg.core.validators import derive_names, validate_flutter_package +from flet_pkg.mcp._serializers import to_dict + +# --------------------------------------------------------------------------- +# Lifespan — share a single PubDevDownloader instance across tools +# --------------------------------------------------------------------------- + + +@dataclass +class AppContext: + downloader: PubDevDownloader + + +@asynccontextmanager +async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: + """Provide a shared ``PubDevDownloader`` instance for the server lifetime.""" + yield AppContext(downloader=PubDevDownloader()) + + +# --------------------------------------------------------------------------- +# Server +# --------------------------------------------------------------------------- + +mcp = FastMCP( + name="flet-pkg", + instructions=("Scaffold and analyze Flet extension packages from Flutter packages on pub.dev."), + lifespan=app_lifespan, +) + +# Type alias for tool context +ToolContext = Context[ServerSession, AppContext] + + +def _get_downloader(ctx: ToolContext) -> PubDevDownloader: + """Extract the shared downloader from the MCP tool context.""" + return ctx.request_context.lifespan_context.downloader + + +# --------------------------------------------------------------------------- +# Tool 1: derive_names +# --------------------------------------------------------------------------- + + +@mcp.tool() +def tool_derive_names(flutter_package: str) -> dict: + """Derive project/package/control names from a Flutter package name. + + Args: + flutter_package: A pub.dev package name (e.g. ``onesignal_flutter``). + + Returns: + Dict with ``project_name``, ``package_name``, ``control_name``, + and ``control_name_snake``. + """ + error = validate_flutter_package(flutter_package) + if error: + raise ValueError(f"Invalid flutter_package '{flutter_package}': {error}") + names = derive_names(flutter_package) + return to_dict(names) # type: ignore[return-value] + + +# --------------------------------------------------------------------------- +# Tool 2: map_dart_type +# --------------------------------------------------------------------------- + + +@mcp.tool() +def tool_map_dart_type(dart_type: str, flet_aware: bool = False) -> dict: + """Convert a Dart type string to its Python/Flet equivalent. + + Args: + dart_type: Dart type string (e.g. ``"List"``, ``"bool?"``). + flet_aware: Use native Flet types (``ft.Alignment``, ``ft.Color``, etc.) + for UI control extensions. + + Returns: + Dict with ``dart_type``, ``python_type``, and ``skipped`` flag. + """ + if flet_aware: + result = map_dart_type_flet(dart_type) + skipped = result is None + return { + "dart_type": dart_type, + "python_type": result if result is not None else "SKIPPED", + "skipped": skipped, + } + return { + "dart_type": dart_type, + "python_type": map_dart_type(dart_type), + "skipped": False, + } + + +# --------------------------------------------------------------------------- +# Tool 3: fetch_metadata +# --------------------------------------------------------------------------- + + +@mcp.tool() +def tool_fetch_metadata(flutter_package: str, ctx: ToolContext) -> dict: + """Get pub.dev package metadata (version, description, homepage). + + Args: + flutter_package: A pub.dev package name. + ctx: MCP tool context for dependency injection. + + Returns: + Dict with package metadata fields. + """ + downloader = _get_downloader(ctx) + metadata = downloader.fetch_metadata(flutter_package) + return to_dict(metadata) # type: ignore[return-value] + + +# --------------------------------------------------------------------------- +# Tool 4: detect_extension_type +# --------------------------------------------------------------------------- + + +@mcp.tool() +def tool_detect_extension_type(flutter_package: str, ctx: ToolContext) -> dict: + """Auto-detect whether a Flutter package should be a service or ui_control extension. + + Downloads the package (cached) and scans for widget classes. + + Args: + flutter_package: A pub.dev package name. + ctx: MCP tool context for dependency injection. + + Returns: + Dict with ``flutter_package`` and ``extension_type``. + """ + from flet_pkg.core.parser import detect_extension_type + + downloader = _get_downloader(ctx) + package_path = downloader.download(flutter_package) + ext_type = detect_extension_type(package_path) + return {"flutter_package": flutter_package, "extension_type": ext_type} + + +# --------------------------------------------------------------------------- +# Tool 5: scaffold +# --------------------------------------------------------------------------- + + +@mcp.tool() +def tool_scaffold( + template_name: str, + flutter_package: str, + project_name: str, + package_name: str, + control_name: str, + description: str = "", + author: str = "", + output_dir: str = ".", + include_console: bool = True, +) -> dict: + """Create a project skeleton from a template (service or ui_control). + + Does NOT generate code — only scaffolds the directory structure with + template stubs. Use run_pipeline after this to fill in the generated code. + """ + from flet_pkg.core.scaffolder import Scaffolder + from flet_pkg.core.validators import _camel_to_snake + + context = { + "project_name": project_name, + "package_name": package_name, + "control_name": control_name, + "control_name_snake": _camel_to_snake(control_name).lower(), + "flutter_package": flutter_package, + "description": description, + "author": author, + "include_console": include_console, + } + + out = Path(output_dir).resolve() + scaffolder = Scaffolder(template_name, context, out) + project_dir = scaffolder.generate() + + files = sorted(str(p.relative_to(project_dir)) for p in project_dir.rglob("*") if p.is_file()) + return {"project_dir": str(project_dir), "files": files} + + +# --------------------------------------------------------------------------- +# Tool 6: run_pipeline +# --------------------------------------------------------------------------- + + +@mcp.tool() +def tool_run_pipeline( + flutter_package: str, + control_name: str, + extension_type: str, + project_dir: str, + package_name: str, + description: str = "", + control_name_snake: str = "", + include_console: bool = True, + local_package_path: str | None = None, +) -> dict: + """Run the full generation pipeline: download → parse → analyze → generate → write. + + Overwrites template stubs in project_dir with auto-generated code. + Set local_package_path to use a local Flutter package instead of downloading. + """ + from flet_pkg.core.pipeline import GenerationPipeline + + pipeline = GenerationPipeline() + result = pipeline.run( + flutter_package=flutter_package, + control_name=control_name, + extension_type=extension_type, + project_dir=Path(project_dir), + package_name=package_name, + description=description, + local_package=Path(local_package_path) if local_package_path else None, + control_name_snake=control_name_snake, + include_console=include_console, + ) + + plan = result.plan + return { + "project_dir": str(result.project_dir), + "files_generated": result.files_generated, + "warnings": result.warnings, + "plan_summary": { + "control_name": plan.control_name, + "base_class": plan.base_class, + "n_methods": len(plan.main_methods), + "n_events": len(plan.events), + "n_enums": len(plan.enums), + "n_sub_modules": len(plan.sub_modules), + "n_sub_controls": len(plan.sub_controls), + "n_properties": len(plan.properties), + }, + } + + +# --------------------------------------------------------------------------- +# Tool 7: analyze_gaps +# --------------------------------------------------------------------------- + + +@mcp.tool() +def tool_analyze_gaps( + flutter_package: str, + extension_type: str, + ctx: ToolContext, +) -> dict: + """Analyze coverage gaps between a Flutter package and its generated code. + + Downloads the package, parses, analyzes, and runs deterministic gap analysis. + No LLM is used — purely structural comparison. + """ + from flet_pkg.core.ai.gap_analyzer import GapAnalyzer + from flet_pkg.core.analyzer import PackageAnalyzer + from flet_pkg.core.parser import parse_dart_package_api + + downloader = _get_downloader(ctx) + package_path = downloader.download(flutter_package) + + api = parse_dart_package_api( + package_path, + include_widgets=(extension_type == "ui_control"), + ) + + analyzer = PackageAnalyzer() + names = derive_names(flutter_package) + plan = analyzer.analyze( + api, + control_name=names.control_name, + extension_type=extension_type, + flutter_package=flutter_package, + package_name=names.package_name, + ) + + gap_analyzer = GapAnalyzer() + report = gap_analyzer.analyze(api, plan, extension_type) + + return { + "flutter_package": flutter_package, + "extension_type": extension_type, + "coverage_pct": round(report.coverage_pct, 1), + "total_dart_api": report.total_dart_api, + "total_generated": report.total_generated, + "feasible_gaps": report.feasible_gaps, + "gaps": [to_dict(g) for g in report.gaps], + } + + +# --------------------------------------------------------------------------- +# Resource 1: type-map +# --------------------------------------------------------------------------- + + +@mcp.resource("flet-pkg://type-map") +def resource_type_map() -> dict: + """Full Dart-to-Python type mapping table. + + Includes standard mappings, Flet-aware mappings, and skipped types. + """ + flet_map = {} + skipped = [] + for dart_type, python_type in _FLET_TYPE_MAP.items(): + if python_type is None: + skipped.append(dart_type) + else: + flet_map[dart_type] = python_type + + return { + "standard": dict(_TYPE_MAP), + "flet_aware": flet_map, + "skipped": skipped, + } + + +# --------------------------------------------------------------------------- +# Resource 2: templates +# --------------------------------------------------------------------------- + + +@mcp.resource("flet-pkg://templates") +def resource_templates() -> dict: + """Available template names and their paths.""" + templates = sorted(d.name for d in TEMPLATE_DIR.iterdir() if d.is_dir()) + return {"templates": templates} + + +# --------------------------------------------------------------------------- +# Prompt 1: scaffold_service +# --------------------------------------------------------------------------- + + +@mcp.prompt() +def scaffold_service(flutter_package: str) -> str: + """Step-by-step guide to scaffold a Flet service extension.""" + pkg = flutter_package + return ( + f"Scaffold a Flet **service** extension for `{pkg}`.\n\n" + "Follow these steps:\n\n" + f'1. Call `tool_derive_names("{pkg}")` to get naming conventions.\n' + f'2. Call `tool_fetch_metadata("{pkg}")` to get the description.\n' + f'3. Call `tool_scaffold(template_name="service", ...)` with derived names.\n' + f'4. Call `tool_run_pipeline(extension_type="service", ...)` to generate.\n' + f'5. Optionally call `tool_analyze_gaps("{pkg}", "service")` for coverage.\n\n' + "Report the generated files and any warnings." + ) + + +# --------------------------------------------------------------------------- +# Prompt 2: scaffold_ui_control +# --------------------------------------------------------------------------- + + +@mcp.prompt() +def scaffold_ui_control(flutter_package: str) -> str: + """Step-by-step guide to scaffold a Flet UI control extension.""" + pkg = flutter_package + return ( + f"Scaffold a Flet **UI control** extension for `{pkg}`.\n\n" + "Follow these steps:\n\n" + f'1. Call `tool_derive_names("{pkg}")` to get naming conventions.\n' + f'2. Call `tool_fetch_metadata("{pkg}")` to get the description.\n' + f'3. Call `tool_scaffold(template_name="ui_control", ...)` with derived names.\n' + f'4. Call `tool_run_pipeline(extension_type="ui_control", ...)` to generate.\n' + f'5. Optionally call `tool_analyze_gaps("{pkg}", "ui_control")` for coverage.\n\n' + "Report the generated files and any warnings." + ) + + +# --------------------------------------------------------------------------- +# Prompt 3: analyze_package +# --------------------------------------------------------------------------- + + +@mcp.prompt() +def analyze_package(flutter_package: str, extension_type: str = "service") -> str: + """Guide to analyze a Flutter package's coverage without scaffolding.""" + pkg = flutter_package + ext = extension_type + return ( + f"Analyze the Flutter package `{pkg}` as a `{ext}` extension.\n\n" + "Follow these steps:\n\n" + f'1. Call `tool_fetch_metadata("{pkg}")` to get package info.\n' + f'2. Call `tool_detect_extension_type("{pkg}")` to verify the type.\n' + f'3. Call `tool_analyze_gaps("{pkg}", "{ext}")` for coverage.\n\n' + "Report:\n" + "- Coverage percentage\n" + "- Number of feasible gaps\n" + "- List of gaps with their kind, Dart name, and reason." + ) + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +def main() -> None: + """Run the flet-pkg MCP server (stdio transport).""" + mcp.run() diff --git a/src/flet_pkg/templates/service/template.yaml b/src/flet_pkg/templates/service/template.yaml new file mode 100644 index 0000000..7204f49 --- /dev/null +++ b/src/flet_pkg/templates/service/template.yaml @@ -0,0 +1,2 @@ +name: service +description: Flet Service extension (non-visual) diff --git a/src/flet_pkg/templates/service/{{project_name}}/CHANGELOG.md.jinja b/src/flet_pkg/templates/service/{{project_name}}/CHANGELOG.md.jinja new file mode 100644 index 0000000..7a5b472 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/CHANGELOG.md.jinja @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.0 + +- Initial release of {{ project_name }}. diff --git a/src/flet_pkg/templates/service/{{project_name}}/LICENSE.jinja b/src/flet_pkg/templates/service/{{project_name}}/LICENSE.jinja new file mode 100644 index 0000000..97da031 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/LICENSE.jinja @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 {{ author }} + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/flet_pkg/templates/service/{{project_name}}/README.md.jinja b/src/flet_pkg/templates/service/{{project_name}}/README.md.jinja new file mode 100644 index 0000000..465fe63 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/README.md.jinja @@ -0,0 +1,80 @@ +# {{ project_name }} + +{{ description }} + +## Installation + +```bash +# Using UV (recommended) +uv add {{ project_name }} + +# Using pip +pip install {{ project_name }} +``` + +## Usage + +```python +{% if include_console -%} +import logging + +{% endif -%} +import flet as ft +from {{ package_name }} import {{ control_name }} +{%- if include_console %} +from {{ package_name }}.console import DebugConsole, setup_logging + +logger = setup_logging(level=logging.DEBUG) +{%- endif %} + + +async def main(page: ft.Page): + page.title = "{{ control_name }} Example" +{%- if include_console %} + + debug = DebugConsole() + page.appbar = ft.AppBar( + title=ft.Text("{{ control_name }} Example"), + actions=[debug.icon], + ) +{%- endif %} + + {{ control_name_snake }} = {{ control_name }}( +{%- if include_console %} + on_error=lambda e: logger.error(f"Error: {e.method}: {e.message}"), +{%- else %} + on_error=lambda e: print(f"Error: {e.method}: {e.message}"), +{%- endif %} + ) + page.services.append({{ control_name_snake }}) + + async def on_click(e): + await {{ control_name_snake }}.example_method("hello") +{%- if include_console %} + + logger.info("App started") +{%- endif %} + + page.add( + ft.Text("{{ control_name }} Service Example"), + ft.ElevatedButton("Call Example Method", on_click=on_click), + ) + + +if __name__ == "__main__": + ft.run(main) +``` + +## Development + +```bash +# Using UV (recommended) +uv sync --dev + +# Using pip +pip install -e ".[dev]" +``` + +## License + +MIT diff --git a/src/flet_pkg/templates/service/{{project_name}}/about_pkg_flet.md.jinja b/src/flet_pkg/templates/service/{{project_name}}/about_pkg_flet.md.jinja new file mode 100644 index 0000000..ae0194f --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/about_pkg_flet.md.jinja @@ -0,0 +1,611 @@ +# About Package for Flet + +> **Auto-generated by [flet-pkg](https://github.com/nicholasgasior/flet-pkg)** +> as part of **{{ project_name }}** — wrapping the Flutter package +> `{{ flutter_package }}`. +> +> This guide explains how Flet extensions work, how to adjust and extend the +> generated code, and how to build, test, and publish your extension. +> Keep this file as a reference throughout development. + +--- + +## Part 1 — Concepts (Beginner) + +### The Simple Idea + +Think of a Flet extension like a **remote control**: + +- **Python** is the remote — it sends commands and receives feedback. +- **Dart** is the TV — it executes the commands using the native Flutter SDK. +- **Flet Engine** is the infrared signal — it carries messages between them. + +You write Python, the user writes Python, and under the hood Flet handles all +the communication with Dart. Nobody needs to learn Dart to _use_ the extension. + +### What is a Flet Extension? + +A Flet extension wraps a native Flutter package so that Python developers can +use it directly from Flet. The extension ships two layers of code: + +| Layer | Language | Role | +|--------|----------|------------------------------------------------------| +| Python | Python | Public API — controls, properties, events, types | +| Dart | Dart | Bridge — receives commands and calls the Flutter SDK | + +### Extension Types + +Flet supports two extension types: + +| Type | Base class | Factory method | Use case | +|----------------|--------------------|-------------------|-----------------------------------| +| **Service** | `ft.Service` | `createService` | Background logic, no UI | +| **UI Control** | `ft.LayoutControl` | `createWidget` | Visual widget rendered on screen | + +**{{ project_name }}** uses the **Service** type (`ft.Service`). + +### How Communication Works (Overview) + +There are three ways Python and Dart talk to each other: + +1. **Commands** (Python → Dart) — Python calls a method, Dart executes it +2. **Events** (Dart → Python) — Dart sends notifications back to Python +3. **Properties** (Python → Dart) — Python sets a value, Dart reads it on next update + +That's it. Every feature in your extension is built from these three patterns. + +--- + +## Part 2 — Implementation (Intermediate) + +### Directory Layout + +``` +{{ project_name }}/ +├── pyproject.toml # Python package config +├── README.md +├── about_pkg_flet.md # This guide +├── src/ +│ ├── {{ package_name }}/ # Python package +│ │ ├── __init__.py # Public exports +│ │ ├── {{ control_name_snake }}.py # Control class +│ │ └── types.py # Event dataclasses +│ └── flutter/ +│ └── {{ package_name }}/ # Dart package +│ ├── pubspec.yaml # Flutter dependencies +│ └── lib/ +│ ├── {{ package_name }}.dart # Barrel export +│ └── src/ +│ ├── extension.dart # Factory registration +│ └── {{ control_name_snake }}_service.dart # Dart bridge +├── examples/ +│ └── {{ package_name }}_example/ +│ └── src/main.py # Example app +├── scripts/ +│ ├── flet_log.py # Android logcat viewer (Python) +│ └── flet_log.sh # Android logcat viewer (Bash) +└── tests/ + └── test_{{ package_name }}.py +``` + +### Python: Control Class + +```python +# src/{{ package_name }}/{{ control_name_snake }}.py +from typing import Optional +import flet as ft +from {{ package_name }}.types import {{ control_name }}ErrorEvent + +@ft.control("{{ control_name }}") +class {{ control_name }}(ft.Service): + """{{ description }}""" + + # Properties — synced to Dart on page.update() + # example_property: str = "" + + on_error: Optional[ft.EventHandler[{{ control_name }}ErrorEvent]] = None + + async def example_method(self, value: str) -> None: + await self._invoke_method("example_method", {"value": value}) +``` + +Key points: +- `@ft.control("{{ control_name }}")` registers the control type name +- `ft.Service` is the base class (no visual output) +- `_invoke_method(name, args)` sends calls to Dart +- Event handlers are typed `Optional[ft.EventHandler[EventType]]` + +### Python: Event Types + +```python +# src/{{ package_name }}/types.py +from dataclasses import dataclass +from typing import Optional +import flet as ft + +@dataclass +class {{ control_name }}ErrorEvent(ft.Event["{{ control_name }}"]): + """Event fired when an error occurs.""" + method: Optional[str] = None + message: Optional[str] = None + stack_trace: Optional[str] = None +``` + +Each event is a `@dataclass` extending `ft.Event[ControlType]`. Field names +must match the keys passed to `triggerEvent()` on the Dart side. + +### Python: `__init__.py` + +```python +# src/{{ package_name }}/__init__.py +from {{ package_name }}.{{ control_name_snake }} import {{ control_name }} +from {{ package_name }}.types import {{ control_name }}ErrorEvent + +__all__ = ["{{ control_name }}", "{{ control_name }}ErrorEvent"] +``` + +### Dart: Service Implementation + +```dart +// src/flutter/{{ package_name }}/lib/src/{{ control_name_snake }}_service.dart +import 'package:flet/flet.dart'; +import 'package:flutter/foundation.dart'; + +class {{ control_name }}Service extends FletService { + {{ control_name }}Service({required super.control}); + + @override + void init() { + super.init(); + control.addInvokeMethodListener(_onInvokeMethod); + } + + Future _onInvokeMethod(String methodName, dynamic args) async { + Map arguments = {}; + if (args != null && args is Map) { + arguments = Map.from(args); + } + try { + return switch (methodName) { + "example_method" => _exampleMethod(arguments), + _ => throw Exception("Unknown method: $methodName"), + }; + } catch (error, stackTrace) { + _handleError(methodName, error, stackTrace); + return null; + } + } + + Future _exampleMethod(Map args) async { + final value = args["value"] as String?; + // Call the actual Flutter SDK here + } + + void _handleError(String method, Object error, StackTrace stackTrace) { + debugPrint("{{ control_name }}Service ERROR in $method: $error"); + control.triggerEvent("error", { + "method": method, + "message": error.toString(), + "stack_trace": stackTrace.toString(), + }); + } +} +``` + +Key points: +- `addInvokeMethodListener` connects method calls from Python +- `switch` dispatches by method name string +- `_handleError` fires an `"error"` event back to Python +- Method name strings must match `_invoke_method` calls in Python + +### Dart: `extension.dart` + +```dart +// src/flutter/{{ package_name }}/lib/src/extension.dart +import 'package:flet/flet.dart'; +import '{{ control_name_snake }}_service.dart'; + +class Extension extends FletExtension { + @override + FletService? createService(Control control) { + switch (control.type) { + case "{{ control_name }}": + return {{ control_name }}Service(control: control); + default: + return null; + } + } +} +``` + +### Dart: Barrel Export + +```dart +// src/flutter/{{ package_name }}/lib/{{ package_name }}.dart +library {{ package_name }}; + +export 'src/extension.dart' show Extension; +``` + +### Configuration: `pubspec.yaml` + +```yaml +name: {{ package_name }} +description: "{{ description }}" +version: 0.1.0 + +environment: + sdk: ^3.5.0 + flutter: ">=3.29.0" + +dependencies: + flutter: + sdk: flutter + flet: ^0.80.5 + {{ flutter_package }}: # Add version constraint +``` + +### Configuration: `pyproject.toml` + +```toml +[project] +name = "{{ project_name }}" +version = "0.1.0" +description = "{{ description }}" +requires-python = ">=3.10" +dependencies = ["flet>=0.80.0"] + +[tool.setuptools.package-data] +"flutter.{{ package_name }}" = ["**/*"] + +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" +``` + +The `package-data` section is critical — it tells setuptools to include the +Dart/Flutter code in the built wheel so Flet can find it at runtime. + +--- + +## Part 3 — Internals (Advanced) + +### Communication Bridge + +``` + Python (your app) Dart (Flutter) + ┌──────────────────┐ ┌──────────────────┐ + │ {{ control_name }} │ ── invoke ──▶ │ {{ control_name }}Service │ + │ │ │ │ + │ on_xxx handler │ ◀── event ─── │ triggerEvent() │ + │ │ │ │ + │ property = val │ ── sync ──▶ │ control.getXxx()│ + └──────────────────┘ └──────────────────┘ + ▲ │ + │ Flet Engine (platform channel) │ + └──────────────────────────────────────────┘ +``` + +### Method Invocation Flow + +When Python calls a method, this is the exact sequence: + +``` +Python Dart +────── ──── +1. await self._invoke_method( 2. _onInvokeMethod(methodName, args) + "do_something", 3. switch (methodName) → _doSomething(args) + {"key": value} 4. Call Flutter SDK + ) 5. Return result / throw +6. Receives return value + or raises exception +``` + +The method name string `"do_something"` must be **identical** on both sides. +Python sends it via `_invoke_method`, Dart receives it in the `switch` +statement. A typo on either side silently fails. + +### Event Flow + +When Dart needs to notify Python (callbacks, state changes, errors): + +``` +Dart Python +──── ────── +1. SDK fires callback 4. on_xxx handler receives +2. Build event data map typed dataclass event +3. control.triggerEvent( 5. Developer processes event + "event_name", data) +``` + +Events arrive as typed dataclasses extending `ft.Event`. For example, +`{{ control_name }}ErrorEvent` carries `method`, `message`, and `stack_trace` fields. + +The event name `"error"` maps to the handler `on_error`. The mapping convention +is: `triggerEvent("some_event", ...)` → `on_some_event` handler in Python. + +### Property Synchronisation + +Properties declared on the Python control class are automatically synced to +the Dart side when `page.update()` is called: + +- **Python** — set `self.some_prop = "value"` +- **Dart** — read `control.getString("some_prop")` + +Common Dart accessors: `getString`, `getInt`, `getDouble`, `getBool`, +`getColor`, `getList`. + +If a property change doesn't seem to reach Dart, make sure `page.update()` +is being called after setting the value. + +### Registration + +Every Flet extension has an `extension.dart` that registers controls with the +engine via a factory method: + +```dart +class Extension extends FletExtension { + @override + FletService? createService(Control control) { + switch (control.type) { + case "{{ control_name }}": + return {{ control_name }}Service(control: control); + default: + return null; + } + } +} +``` + +The `control.type` string must exactly match the string in +`@ft.control("{{ control_name }}")` on the Python side. + +### Type Serialisation + +| Python | Dart | Notes | +|---------------------|--------------------|------------------------------| +| `str` | `String` | | +| `int` | `int` | | +| `float` | `double` | | +| `bool` | `bool` | | +| `list` | `List` | JSON array | +| `dict` | `Map` | JSON object | +| `None` | `null` | | +| `ft.ColorValue` | `Color` | `control.getColor()` | +| `enum` (`.value`) | `String` | Sent as string value | +| `@dataclass` | `Map` | Serialised to/from JSON map | + +--- + +## Part 4 — Install, Test, and Publish (Practical) + +### Installing the Extension + +```bash +# From PyPI (after publishing) +pip install {{ project_name }} + +# From local source (development) +pip install -e . +# or +uv add --editable . +``` + +### Using in Your Flet App + +```python +import flet as ft +from {{ package_name }} import {{ control_name }} + +def main(page: ft.Page): + {{ control_name_snake }} = {{ control_name }}( + on_error=lambda e: print(f"Error: {e.message}"), + ) + page.services.add({{ control_name_snake }}) + + page.add(ft.Text("{{ control_name }} Service")) + +ft.run(main) +``` + +### Building for All Platforms + +```bash +# Desktop +flet build macos +flet build windows +flet build linux + +# Mobile +flet build apk # Android APK +flet build aab # Android App Bundle +flet build ipa # iOS + +# Web +flet build web +``` + +### Local Testing with Example App + +```bash +cd examples/{{ package_name }}_example +pip install -e ../../ # Install extension from source +flet run src/main.py +``` + +Or with path dependency in the example's `pyproject.toml`: + +```toml +[project] +dependencies = [ + "{{ project_name }} @ file:///${PROJECT_ROOT}/../..", +] +``` + +### Testing with a Built Wheel + +You can build a `.whl` (wheel) file and reference it directly as a dependency. +This is useful for testing the exact artifact that will be published to PyPI, +or for sharing pre-built packages without publishing. + +**Step 1 — Build the wheel:** + +```bash +# Using uv (recommended) +uv build + +# Or using standard Python build +python -m build +``` + +This creates a `dist/` directory with files like: + +``` +dist/ +├── {{ project_name }}-0.1.0-py3-none-any.whl +└── {{ project_name }}-0.1.0.tar.gz +``` + +**Step 2 — Reference the `.whl` in another project's `pyproject.toml`:** + +```toml +[project] +dependencies = [ + "flet>=0.80.5", + "{{ project_name }} @ file:///absolute/path/to/{{ project_name }}/dist/{{ package_name }}-0.1.0-py3-none-any.whl", +] +``` + +Then sync the project: + +```bash +uv sync +# or +pip install -e . +``` + +This installs the exact wheel build, including all Dart/Flutter code bundled +inside. It behaves identically to a PyPI install, so `flet build` will find +the Flutter package correctly. + +### Development Dependencies + +For development, use an editable install with path or git dependency: + +```toml +# Path dependency (local dev — installs from source, reflects changes) +dependencies = [ + "{{ project_name }} @ file:///path/to/{{ project_name }}", +] + +# Wheel dependency (local dev — installs a specific build snapshot) +dependencies = [ + "{{ project_name }} @ file:///path/to/{{ project_name }}/dist/{{ package_name }}-0.1.0-py3-none-any.whl", +] + +# Git dependency +dependencies = [ + "{{ project_name }} @ git+https://github.com/youruser/{{ project_name }}.git", +] +``` + +### Publishing to PyPI + +```bash +# Build wheel and sdist +uv build +# or: python -m build + +# Publish with uv (recommended — builds + uploads in one step) +uv publish +# Prompts: Enter username ('__token__' if using a token): + +# Or publish with twine +twine upload dist/* + +# Or upload to Test PyPI first +uv publish --publish-url https://test.pypi.org/legacy/ +# or: twine upload --repository testpypi dist/* +``` + +After publishing, users install with: + +```bash +pip install {{ project_name }} +``` + +--- + +## Debugging with Android LogCat + +The `scripts/` directory includes two logcat viewers (Python and Bash) that +filter Android logs for Flet/Flutter-related output. They auto-detect the +focused app and colour-code log levels like Android Studio. + +```bash +# Python version +python scripts/flet_log.py + +# Bash version +./scripts/flet_log.sh + +# Add an extra filter for your Flutter package +python scripts/flet_log.py "{{ flutter_package }}" +./scripts/flet_log.sh "{{ flutter_package }}" +``` + +**Requirements:** `adb` must be installed and your Android device connected +via USB or Wi-Fi debugging. + +**Default filter:** `flutter | python | Error | Exception | Traceback` + +The scripts automatically: +- Detect which app is in the foreground +- Filter logs by the app's PID +- Re-attach when the app restarts or focus changes +- Format output with colour-coded log levels (V/D/I/W/E/F) + +### Printing to LogCat from Dart + +Use `debugPrint()` or `print()` in your Dart code to send messages to LogCat. +Both appear in the filtered output of `flet_log.py` / `flet_log.sh`: + +```dart +import 'package:flutter/foundation.dart'; + +// Preferred — truncates long output to avoid log overflow +debugPrint("{{ control_name }}Service: method called with value=$value"); + +// Also works — simpler but no truncation protection +print("{{ control_name }}Service: something happened"); +``` + +`debugPrint` is recommended over `print` because it throttles output and +truncates very long strings, preventing log buffer overflow on Android. +The generated `_handleError` method already uses `debugPrint` for error +logging. + +--- + +## Debugging Checklist + +If something doesn't work, walk through this list: + +- [ ] **Control name match** — `@ft.control("{{ control_name }}")` in Python matches + `case "{{ control_name }}"` in Dart's `extension.dart` +- [ ] **Method names match** — the string in `_invoke_method("method_name", ...)` + matches exactly in the Dart `switch` statement +- [ ] **Event names match** — `control.triggerEvent("event_name", ...)` in Dart + maps to `on_event_name` handler in Python +- [ ] **`page.update()` called** — property changes only reach Dart after calling + `page.update()` (or `await page.update_async()`) +- [ ] **Event dataclass fields match** — field names in the Python `@dataclass` + match the keys in the Dart `triggerEvent` data map +- [ ] **Package-data included** — `pyproject.toml` has `[tool.setuptools.package-data]` + with `"flutter.{{ package_name }}" = ["**/*"]` +- [ ] **Extension registered** — `extension.dart` exists and the barrel file + (`{{ package_name }}.dart`) exports it +- [ ] **Flutter dependency added** — `pubspec.yaml` lists `{{ flutter_package }}` + under `dependencies` +- [ ] **Rebuild after Dart changes** — Dart code changes require `flet build` + again; only Python changes are hot-reloaded diff --git a/src/flet_pkg/templates/service/{{project_name}}/docs/api-reference.md.jinja b/src/flet_pkg/templates/service/{{project_name}}/docs/api-reference.md.jinja new file mode 100644 index 0000000..4b139db --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/docs/api-reference.md.jinja @@ -0,0 +1,76 @@ +# API Reference + +## {{ control_name }} + +`{{ control_name }}` is a Flet Service extension based on `ft.Service`. + +### Import + +```python +from {{ package_name }} import {{ control_name }} +``` + +### Properties + +TODO: Document properties here. + +### Methods + +TODO: Document methods here. + +### Events + +TODO: Document event handlers here. + +### Example + +```python +{% if include_console -%} +import logging + +{% endif -%} +import flet as ft +from {{ package_name }} import {{ control_name }} +{%- if include_console %} +from {{ package_name }}.console import DebugConsole, setup_logging + +logger = setup_logging(level=logging.DEBUG) +{%- endif %} + + +async def main(page: ft.Page): + page.title = "{{ control_name }} Example" +{%- if include_console %} + + debug = DebugConsole() + page.appbar = ft.AppBar( + title=ft.Text("{{ control_name }} Example"), + actions=[debug.icon], + ) +{%- endif %} + + {{ control_name_snake }} = {{ control_name }}( +{%- if include_console %} + on_error=lambda e: logger.error(f"Error: {e.method}: {e.message}"), +{%- else %} + on_error=lambda e: print(f"Error: {e.method}: {e.message}"), +{%- endif %} + ) + page.services.append({{ control_name_snake }}) + + async def on_click(e): + await {{ control_name_snake }}.example_method("hello") +{%- if include_console %} + + logger.info("App started") +{%- endif %} + + page.add( + ft.Text("{{ control_name }} Service Example"), + ft.ElevatedButton("Call Example Method", on_click=on_click), + ) + + +if __name__ == "__main__": + ft.run(main) +``` diff --git a/src/flet_pkg/templates/service/{{project_name}}/docs/changelog.md.jinja b/src/flet_pkg/templates/service/{{project_name}}/docs/changelog.md.jinja new file mode 100644 index 0000000..7a5b472 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/docs/changelog.md.jinja @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.0 + +- Initial release of {{ project_name }}. diff --git a/src/flet_pkg/templates/service/{{project_name}}/docs/examples.md.jinja b/src/flet_pkg/templates/service/{{project_name}}/docs/examples.md.jinja new file mode 100644 index 0000000..3dff893 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/docs/examples.md.jinja @@ -0,0 +1,58 @@ +# Examples + +## Basic Usage + +```python +{% if include_console -%} +import logging + +{% endif -%} +import flet as ft +from {{ package_name }} import {{ control_name }} +{%- if include_console %} +from {{ package_name }}.console import DebugConsole, setup_logging + +logger = setup_logging(level=logging.DEBUG) +{%- endif %} + + +async def main(page: ft.Page): + page.title = "{{ control_name }} Example" +{%- if include_console %} + + debug = DebugConsole() + page.appbar = ft.AppBar( + title=ft.Text("{{ control_name }} Example"), + actions=[debug.icon], + ) +{%- endif %} + + {{ control_name_snake }} = {{ control_name }}( +{%- if include_console %} + on_error=lambda e: logger.error(f"Error: {e.method}: {e.message}"), +{%- else %} + on_error=lambda e: print(f"Error: {e.method}: {e.message}"), +{%- endif %} + ) + page.services.append({{ control_name_snake }}) + + async def on_click(e): + await {{ control_name_snake }}.example_method("hello") +{%- if include_console %} + + logger.info("App started") +{%- endif %} + + page.add( + ft.Text("{{ control_name }} Service Example"), + ft.ElevatedButton("Call Example Method", on_click=on_click), + ) + + +if __name__ == "__main__": + ft.run(main) +``` + +## More Examples + +TODO: Add more usage examples here. diff --git a/src/flet_pkg/templates/service/{{project_name}}/docs/getting-started.md.jinja b/src/flet_pkg/templates/service/{{project_name}}/docs/getting-started.md.jinja new file mode 100644 index 0000000..a5abcd0 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/docs/getting-started.md.jinja @@ -0,0 +1,70 @@ +# Getting Started + +## Installation + +```bash +# Using UV (recommended) +uv add {{ project_name }} + +# Using pip +pip install {{ project_name }} +``` + +## Basic Usage + +```python +{% if include_console -%} +import logging + +{% endif -%} +import flet as ft +from {{ package_name }} import {{ control_name }} +{%- if include_console %} +from {{ package_name }}.console import DebugConsole, setup_logging + +logger = setup_logging(level=logging.DEBUG) +{%- endif %} + + +async def main(page: ft.Page): + page.title = "{{ control_name }} Example" +{%- if include_console %} + + debug = DebugConsole() + page.appbar = ft.AppBar( + title=ft.Text("{{ control_name }} Example"), + actions=[debug.icon], + ) +{%- endif %} + + {{ control_name_snake }} = {{ control_name }}( +{%- if include_console %} + on_error=lambda e: logger.error(f"Error: {e.method}: {e.message}"), +{%- else %} + on_error=lambda e: print(f"Error: {e.method}: {e.message}"), +{%- endif %} + ) + page.services.append({{ control_name_snake }}) + + async def on_click(e): + await {{ control_name_snake }}.example_method("hello") +{%- if include_console %} + + logger.info("App started") +{%- endif %} + + page.add( + ft.Text("{{ control_name }} Service Example"), + ft.ElevatedButton("Call Example Method", on_click=on_click), + ) + + +if __name__ == "__main__": + ft.run(main) +``` + +## Next Steps + +- Check the [API Reference](api-reference.md) for all available methods and properties. +- See the [Examples](examples.md) for more usage patterns. +- See the [Changelog](changelog.md) for version history. diff --git a/src/flet_pkg/templates/service/{{project_name}}/docs/index.md.jinja b/src/flet_pkg/templates/service/{{project_name}}/docs/index.md.jinja new file mode 100644 index 0000000..6f6bde9 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/docs/index.md.jinja @@ -0,0 +1,68 @@ +# {{ project_name }} + +{{ description }} + +## Overview + +`{{ project_name }}` is a Flet Service extension that provides the `{{ control_name }}` service for your Flet applications. + +## Quick Start + +```bash +# Using UV (recommended) +uv add {{ project_name }} + +# Using pip +pip install {{ project_name }} +``` + +```python +{% if include_console -%} +import logging + +{% endif -%} +import flet as ft +from {{ package_name }} import {{ control_name }} +{%- if include_console %} +from {{ package_name }}.console import DebugConsole, setup_logging + +logger = setup_logging(level=logging.DEBUG) +{%- endif %} + + +async def main(page: ft.Page): + page.title = "{{ control_name }} Example" +{%- if include_console %} + + debug = DebugConsole() + page.appbar = ft.AppBar( + title=ft.Text("{{ control_name }} Example"), + actions=[debug.icon], + ) +{%- endif %} + + {{ control_name_snake }} = {{ control_name }}( +{%- if include_console %} + on_error=lambda e: logger.error(f"Error: {e.method}: {e.message}"), +{%- else %} + on_error=lambda e: print(f"Error: {e.method}: {e.message}"), +{%- endif %} + ) + page.services.append({{ control_name_snake }}) + + async def on_click(e): + await {{ control_name_snake }}.example_method("hello") +{%- if include_console %} + + logger.info("App started") +{%- endif %} + + page.add( + ft.Text("{{ control_name }} Service Example"), + ft.ElevatedButton("Call Example Method", on_click=on_click), + ) + + +if __name__ == "__main__": + ft.run(main) +``` diff --git a/src/flet_pkg/templates/service/{{project_name}}/examples/{{package_name}}_example/pyproject.toml.jinja b/src/flet_pkg/templates/service/{{project_name}}/examples/{{package_name}}_example/pyproject.toml.jinja new file mode 100644 index 0000000..9bcbcc2 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/examples/{{package_name}}_example/pyproject.toml.jinja @@ -0,0 +1,19 @@ +[project] +name = "{{ package_name }}-example" +version = "0.1.0" +description = "Example app for {{ project_name }}" +requires-python = ">=3.10" +dependencies = [ + "flet>=0.80.0", + "{{ project_name }}>=0.1.0", +] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.flet.app] +path = "src" + +[tool.flet.dependencies] +{{ project_name }} = { path = "../../" } diff --git a/src/flet_pkg/templates/service/{{project_name}}/examples/{{package_name}}_example/src/main.py.jinja b/src/flet_pkg/templates/service/{{project_name}}/examples/{{package_name}}_example/src/main.py.jinja new file mode 100644 index 0000000..d510293 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/examples/{{package_name}}_example/src/main.py.jinja @@ -0,0 +1,49 @@ +{% if include_console -%} +import logging + +{% endif -%} +import flet as ft +from {{ package_name }} import {{ control_name }} +{%- if include_console %} +from {{ package_name }}.console import DebugConsole, setup_logging + +logger = setup_logging(level=logging.DEBUG) +{%- endif %} + + +async def main(page: ft.Page): + page.title = "{{ control_name }} Example" +{%- if include_console %} + + debug = DebugConsole() + page.appbar = ft.AppBar( + title=ft.Text("{{ control_name }} Example"), + actions=[debug.icon], + ) +{%- endif %} + + {{ control_name_snake }} = {{ control_name }}( +{%- if include_console %} + on_error=lambda e: logger.error(f"Error: {e.method}: {e.message}"), +{%- else %} + on_error=lambda e: print(f"Error: {e.method}: {e.message}"), +{%- endif %} + ) + page.services.append({{ control_name_snake }}) + + async def on_click(e): + """Handle button click. Change this to call your extension's method.""" + await {{ control_name_snake }}.example_method("hello") +{%- if include_console %} + + logger.info("App started") +{%- endif %} + + page.add( + ft.Text("{{ control_name }} Service Example"), + ft.ElevatedButton("Call Example Method", on_click=on_click), + ) + + +if __name__ == "__main__": + ft.run(main) diff --git a/src/flet_pkg/templates/service/{{project_name}}/mkdocs.yml.jinja b/src/flet_pkg/templates/service/{{project_name}}/mkdocs.yml.jinja new file mode 100644 index 0000000..b6524cf --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/mkdocs.yml.jinja @@ -0,0 +1,25 @@ +site_name: {{ project_name }} +site_description: {{ description }} + +theme: + name: material + palette: + - scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + +nav: + - Home: index.md + - Getting Started: getting-started.md + - API Reference: api-reference.md + - Examples: examples.md + - Changelog: changelog.md diff --git a/src/flet_pkg/templates/service/{{project_name}}/pyproject.toml.jinja b/src/flet_pkg/templates/service/{{project_name}}/pyproject.toml.jinja new file mode 100644 index 0000000..11cc386 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/pyproject.toml.jinja @@ -0,0 +1,28 @@ +[project] +name = "{{ project_name }}" +version = "0.1.0" +description = "{{ description }}" +readme = "README.md" +requires-python = ">=3.10" +license = { text = "MIT" } +authors = [ + { name = "{{ author }}" }, +] +dependencies = [ + "flet>=0.80.0", +] + +[project.urls] +Homepage = "https://github.com/{{ author }}/{{ project_name }}" +Documentation = "https://github.com/{{ author }}/{{ project_name }}" +Repository = "https://github.com/{{ author }}/{{ project_name }}" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +"flutter.{{ package_name }}" = ["**/*"] + +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" diff --git a/src/flet_pkg/templates/service/{{project_name}}/scripts/flet_log.py b/src/flet_pkg/templates/service/{{project_name}}/scripts/flet_log.py new file mode 100644 index 0000000..1917989 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/scripts/flet_log.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +""" +Flet Logcat - Android Studio Style + +Author: Bruno Brown (https://github.com/BrunoBrown) + +A Python script for monitoring Android logcat with Flet/Flutter app filtering. +Automatically detects the focused app and filters relevant logs. + +Usage: python flet_log.py [extra_filter] +Example: python flet_log.py "your_flutter_package" +""" + +import os +import re +import subprocess +import sys +import threading +import time +from typing import Optional + +# Colors by log level (Android Studio style) +COLORS = { + "V": "\033[0;37m", # Verbose - Light gray + "D": "\033[0;36m", # Debug - Cyan + "I": "\033[0;32m", # Info - Green + "W": "\033[0;33m", # Warning - Yellow + "E": "\033[0;31m", # Error - Red + "F": "\033[1;31m", # Fatal - Bold red + "A": "\033[1;31m", # Assert - Bold red +} +RESET = "\033[0m" +BOLD = "\033[1m" +DIM = "\033[2m" + +# Default filter +DEFAULT_FILTER = r"flutter|python|Error|Exception|Traceback" + +# Global state +current_process: Optional[subprocess.Popen] = None +stop_event = threading.Event() + + +def run_cmd(cmd: str) -> str: + """Run a shell command and return output.""" + result = subprocess.run( + cmd, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + ) + return result.stdout.strip() + + +def get_current_package() -> Optional[str]: + """Get the currently focused app package name.""" + output = run_cmd("adb shell dumpsys activity activities 2>/dev/null") + for line in output.splitlines(): + if "mResumedActivity" in line or "mFocusedActivity" in line: + match = re.search(r"[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z0-9_]+)+", line) + if match: + return match.group(0) + return None + + +def get_pid(package: str) -> Optional[str]: + """Get the PID of a package.""" + pid = run_cmd(f"adb shell pidof {package} 2>/dev/null") + return pid.split()[0] if pid else None + + +def format_log_line(line: str) -> Optional[str]: + """Format a log line in Android Studio style.""" + # Format: MM-DD HH:MM:SS.mmm PID TID LEVEL TAG: MESSAGE + pattern = ( + r"^(\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2}\.\d+)\s+" + r"(\d+)\s+(\d+)\s+([VDIWEFA])\s+([^:]+):\s+(.*)$" + ) + match = re.match(pattern, line) + + if match: + date, time_str, pid, tid, level, tag, msg = match.groups() + color = COLORS.get(level, RESET) + # Format: TIME PID-TID TAG LEVEL: MESSAGE + return ( + f"{DIM}{time_str}{RESET} {DIM}{pid:>5}-{tid:<5}{RESET} " + f"{color}{tag:<20} {level}: {msg}{RESET}" + ) + else: + # Unformatted line (continuation or different format) + return f"{DIM}{line}{RESET}" if line.strip() else None + + +def print_header(pkg: str, pid: Optional[str]) -> None: + """Print app header.""" + print() + print(f"{BOLD}━━━ {pkg} {DIM}PID: {pid or '?'}{RESET}") + print() + + +def print_usage(extra_filter: Optional[str]) -> None: + """Print usage instructions.""" + os.system("clear" if os.name == "posix" else "cls") + print(f"{BOLD}Logcat{RESET} {DIM}| Flet/Flutter{RESET}") + print() + print(f"{DIM}Usage: python flet_log.py [extra_filter]{RESET}") + print(f'{DIM}Example: python flet_log.py "your_flutter_package"{RESET}') + print() + print(f"{DIM}Default filter: flutter, python, Error, Exception, Traceback{RESET}") + if extra_filter: + print(f"{DIM}Extra filter: {extra_filter}{RESET}") + print(f"{DIM}Press Ctrl+C to exit{RESET}") + + +def logcat_thread(pid: Optional[str], pkg: str, filter_pattern: str) -> None: + """Thread to read and display logcat output.""" + global current_process + + try: + if pid: + cmd = ["adb", "logcat", "--pid", pid, "-v", "threadtime"] + else: + cmd = ["adb", "logcat", "-v", "threadtime"] + + current_process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + bufsize=1, + ) + + # Compile filter pattern + if pid: + pattern = re.compile(filter_pattern, re.IGNORECASE) + else: + pattern = re.compile(f"{pkg}|{filter_pattern}", re.IGNORECASE) + + if current_process.stdout is None: + return + + for line in current_process.stdout: + if stop_event.is_set(): + break + + line = line.rstrip() + if pattern.search(line): + formatted = format_log_line(line) + if formatted: + print(formatted) + + except Exception: + pass + finally: + if current_process: + current_process.terminate() + current_process = None + + +def main() -> None: + """Main entry point.""" + global current_process + + # Get extra filter from command line + extra_filter = sys.argv[1] if len(sys.argv) > 1 else None + + # Build filter pattern + filter_pattern = DEFAULT_FILTER + if extra_filter: + filter_pattern = f"{filter_pattern}|{extra_filter}" + + # Print usage + print_usage(extra_filter) + + current_pkg = "" + current_pid = "" + logcat_thread_handle: Optional[threading.Thread] = None + + try: + while True: + pkg = get_current_package() + + if not pkg: + time.sleep(1) + continue + + pid = get_pid(pkg) + + if pkg != current_pkg or pid != current_pid: + # Stop current logcat + stop_event.set() + if current_process: + current_process.terminate() + if logcat_thread_handle and logcat_thread_handle.is_alive(): + logcat_thread_handle.join(timeout=1) + + stop_event.clear() + current_pkg = pkg + current_pid = pid or "" + + print_header(pkg, pid) + + # Clear logcat + run_cmd("adb logcat -c 2>/dev/null") + + # Start new logcat thread + logcat_thread_handle = threading.Thread( + target=logcat_thread, + args=(pid, pkg, filter_pattern), + daemon=True, + ) + logcat_thread_handle.start() + + time.sleep(1) + + except KeyboardInterrupt: + pass + finally: + print() + print(f"{DIM}Stopped{RESET}") + stop_event.set() + if current_process: + current_process.terminate() + + +if __name__ == "__main__": + main() diff --git a/src/flet_pkg/templates/service/{{project_name}}/scripts/flet_log.sh b/src/flet_pkg/templates/service/{{project_name}}/scripts/flet_log.sh new file mode 100755 index 0000000..b90f869 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/scripts/flet_log.sh @@ -0,0 +1,128 @@ +#!/bin/bash + +# ══════════════════════════════════════════════════════════════════════════════ +# Flet Logcat - Android Studio Style +# Author: Bruno Brown (https://github.com/BrunoBrown) +# ══════════════════════════════════════════════════════════════════════════════ +# Usage: ./flet_log.sh [filter] +# Example: ./flet_log.sh "your_flutter_package" +# ══════════════════════════════════════════════════════════════════════════════ + +# Colors by log level (Android Studio style) +declare -A COLORS=( + [V]="\033[0;37m" # Verbose - Light gray + [D]="\033[0;36m" # Debug - Cyan + [I]="\033[0;32m" # Info - Green + [W]="\033[0;33m" # Warning - Yellow + [E]="\033[0;31m" # Error - Red + [F]="\033[1;31m" # Fatal - Bold red + [A]="\033[1;31m" # Assert - Bold red +) +RESET="\033[0m" +BOLD="\033[1m" +DIM="\033[2m" + +EXTRA_FILTER="${1:-}" +FILTER="flutter|python|Error|Exception|Traceback" +[[ -n "$EXTRA_FILTER" ]] && FILTER="$FILTER|$EXTRA_FILTER" + +get_package() { + adb shell dumpsys activity activities 2>/dev/null | \ + grep -E "mResumedActivity|mFocusedActivity" | \ + head -1 | \ + grep -oE '[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z0-9_]+)+' | \ + head -1 +} + +get_pid() { + adb shell pidof "$1" 2>/dev/null | tr -d '\r\n' | awk '{print $1}' +} + +# Format and colorize in Android Studio style +format_log() { + while IFS= read -r line; do + # Format: MM-DD HH:MM:SS.mmm PID TID LEVEL TAG: MESSAGE + # Regex to extract components + if [[ "$line" =~ ^([0-9]{2}-[0-9]{2})\ ([0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+)\ +([0-9]+)\ +([0-9]+)\ ([VDIWEFA])\ ([^:]+):\ (.*)$ ]]; then + date="${BASH_REMATCH[1]}" + time="${BASH_REMATCH[2]}" + pid="${BASH_REMATCH[3]}" + tid="${BASH_REMATCH[4]}" + level="${BASH_REMATCH[5]}" + tag="${BASH_REMATCH[6]}" + msg="${BASH_REMATCH[7]}" + + # Color based on level + color="${COLORS[$level]:-$RESET}" + + # Format: TIME PID-TID/TAG LEVEL: MESSAGE + printf "${DIM}%s${RESET} ${DIM}%5s-%-5s${RESET} ${color}%-20s %s: %s${RESET}\n" \ + "$time" "$pid" "$tid" "$tag" "$level" "$msg" + else + # Unformatted line (continuation or different format) + echo -e "${DIM}$line${RESET}" + fi + done +} + +print_header() { + local pkg=$1 + local pid=$2 + echo "" + echo -e "${BOLD}━━━ $pkg ${DIM}PID: ${pid:-?}${RESET}" + echo "" +} + +cleanup() { + echo "" + echo -e "${DIM}Stopped${RESET}" + pkill -P $$ 2>/dev/null + exit 0 +} + +trap cleanup SIGINT SIGTERM + +# Initial header +clear +echo -e "${BOLD}Logcat${RESET} ${DIM}| Flet/Flutter${RESET}" +echo "" +echo -e "${DIM}Usage: ./flet_log.sh [extra_filter]${RESET}" +echo -e "${DIM}Example: ./flet_log.sh \"OneSignal|Firebase\"${RESET}" +echo "" +echo -e "${DIM}Default filter: flutter, python, Error, Exception, Traceback${RESET}" +[[ -n "$EXTRA_FILTER" ]] && echo -e "${DIM}Extra filter: $EXTRA_FILTER${RESET}" +echo -e "${DIM}Press Ctrl+C to exit${RESET}" + +CURRENT_PKG="" +CURRENT_PID="" + +while true; do + PKG=$(get_package) + + [[ -z "$PKG" ]] && { sleep 1; continue; } + + PID=$(get_pid "$PKG") + + if [[ "$PKG" != "$CURRENT_PKG" || "$PID" != "$CURRENT_PID" ]]; then + pkill -P $$ -f "adb logcat" 2>/dev/null + sleep 0.2 + + CURRENT_PKG="$PKG" + CURRENT_PID="$PID" + + print_header "$PKG" "$PID" + adb logcat -c 2>/dev/null + + if [[ -n "$PID" ]]; then + adb logcat --pid="$PID" -v threadtime 2>/dev/null | \ + grep -E --line-buffered -i "$FILTER" | \ + format_log & + else + adb logcat -v threadtime 2>/dev/null | \ + grep -E --line-buffered -i "$PKG|$FILTER" | \ + format_log & + fi + fi + + sleep 1 +done \ No newline at end of file diff --git a/src/flet_pkg/templates/service/{{project_name}}/src/flutter/__init__.py b/src/flet_pkg/templates/service/{{project_name}}/src/flutter/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/src/flutter/__init__.py @@ -0,0 +1 @@ + diff --git a/src/flet_pkg/templates/service/{{project_name}}/src/flutter/{{package_name}}/__init__.py b/src/flet_pkg/templates/service/{{project_name}}/src/flutter/{{package_name}}/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/src/flutter/{{package_name}}/__init__.py @@ -0,0 +1 @@ + diff --git a/src/flet_pkg/templates/service/{{project_name}}/src/flutter/{{package_name}}/analysis_options.yaml b/src/flet_pkg/templates/service/{{project_name}}/src/flutter/{{package_name}}/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/src/flutter/{{package_name}}/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/src/flet_pkg/templates/service/{{project_name}}/src/flutter/{{package_name}}/lib/src/extension.dart.jinja b/src/flet_pkg/templates/service/{{project_name}}/src/flutter/{{package_name}}/lib/src/extension.dart.jinja new file mode 100644 index 0000000..84ae8a0 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/src/flutter/{{package_name}}/lib/src/extension.dart.jinja @@ -0,0 +1,14 @@ +import 'package:flet/flet.dart'; +import '{{ control_name_snake }}_service.dart'; + +class Extension extends FletExtension { + @override + FletService? createService(Control control) { + switch (control.type) { + case "{{ control_name }}": + return {{ control_name }}Service(control: control); + default: + return null; + } + } +} diff --git a/src/flet_pkg/templates/service/{{project_name}}/src/flutter/{{package_name}}/lib/src/{{control_name_snake}}_service.dart.jinja b/src/flet_pkg/templates/service/{{project_name}}/src/flutter/{{package_name}}/lib/src/{{control_name_snake}}_service.dart.jinja new file mode 100644 index 0000000..7de91a9 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/src/flutter/{{package_name}}/lib/src/{{control_name_snake}}_service.dart.jinja @@ -0,0 +1,54 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/foundation.dart'; + +class {{ control_name }}Service extends FletService { + {{ control_name }}Service({required super.control}); + + @override + void init() { + super.init(); + control.addInvokeMethodListener(_onInvokeMethod); + } + + @override + Future update() async { + // Handle property updates from Python side + } + + @override + void dispose() { + super.dispose(); + } + + Future _onInvokeMethod(String methodName, dynamic args) async { + Map arguments = {}; + if (args != null && args is Map) { + arguments = Map.from(args); + } + + try { + return switch (methodName) { + "example_method" => _exampleMethod(arguments), + _ => throw Exception("Unknown method: $methodName"), + }; + } catch (error, stackTrace) { + _handleError(methodName, error, stackTrace); + return null; + } + } + + Future _exampleMethod(Map args) async { + final value = args["value"] as String?; + debugPrint("{{ control_name }}Service: example_method called with value=$value"); + // TODO: Call the actual Flutter package SDK here + } + + void _handleError(String method, Object error, StackTrace stackTrace) { + debugPrint("{{ control_name }}Service ERROR in $method: $error"); + control.triggerEvent("error", { + "method": method, + "message": error.toString(), + "stack_trace": stackTrace.toString(), + }); + } +} diff --git a/src/flet_pkg/templates/service/{{project_name}}/src/flutter/{{package_name}}/lib/{{package_name}}.dart.jinja b/src/flet_pkg/templates/service/{{project_name}}/src/flutter/{{package_name}}/lib/{{package_name}}.dart.jinja new file mode 100644 index 0000000..61c146e --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/src/flutter/{{package_name}}/lib/{{package_name}}.dart.jinja @@ -0,0 +1,3 @@ +library {{ package_name }}; + +export 'src/extension.dart' show Extension; diff --git a/src/flet_pkg/templates/service/{{project_name}}/src/flutter/{{package_name}}/pubspec.yaml.jinja b/src/flet_pkg/templates/service/{{project_name}}/src/flutter/{{package_name}}/pubspec.yaml.jinja new file mode 100644 index 0000000..0a51b4a --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/src/flutter/{{package_name}}/pubspec.yaml.jinja @@ -0,0 +1,20 @@ +name: {{ package_name }} +description: "{{ description }}" +version: 0.1.0 + +environment: + sdk: ^3.5.0 + flutter: ">=3.29.0" + +dependencies: + flutter: + sdk: flutter + flet: ^0.80.5 + {{ flutter_package }}: {{ flutter_package_version | default("any") }} + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: diff --git a/src/flet_pkg/templates/service/{{project_name}}/src/{{package_name}}/__init__.py.jinja b/src/flet_pkg/templates/service/{{project_name}}/src/{{package_name}}/__init__.py.jinja new file mode 100644 index 0000000..a096d55 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/src/{{package_name}}/__init__.py.jinja @@ -0,0 +1,4 @@ +from {{ package_name }}.{{ control_name_snake }} import {{ control_name }} +from {{ package_name }}.types import {{ control_name }}ErrorEvent + +__all__ = ["{{ control_name }}", "{{ control_name }}ErrorEvent"] diff --git a/src/flet_pkg/templates/service/{{project_name}}/src/{{package_name}}/console.py.jinja b/src/flet_pkg/templates/service/{{project_name}}/src/{{package_name}}/console.py.jinja new file mode 100644 index 0000000..ea4d80f --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/src/{{package_name}}/console.py.jinja @@ -0,0 +1,436 @@ +""" +Debug Console module for {{ package_name }}. + +Provides visual debugging tools for viewing application logs directly +in Flet apps during development. Includes file-based logging with rotation +and a log viewer dialog with color-coded levels and filtering. +""" + +import logging +import os +from datetime import datetime +from enum import Enum +from logging.handlers import RotatingFileHandler +from typing import Optional + +import flet as ft + + +class LogLevel(Enum): + """Log level with associated Flet colors for the DebugConsole display.""" + + DEBUG = ("DEBUG", ft.Colors.GREY_600) + INFO = ("INFO", ft.Colors.BLUE_600) + WARNING = ("WARNING", ft.Colors.AMBER_700) + ERROR = ("ERROR", ft.Colors.RED_600) + CRITICAL = ("CRITICAL", ft.Colors.RED_900) + + @property + def color(self) -> str: + return self.value[1] + + @classmethod + def from_string(cls, level: str) -> "LogLevel": + """Get LogLevel from string.""" + level_map = { + "DEBUG": cls.DEBUG, + "INFO": cls.INFO, + "WARNING": cls.WARNING, + "ERROR": cls.ERROR, + "CRITICAL": cls.CRITICAL, + } + return level_map.get(level.upper(), cls.INFO) + + +def get_log_path() -> str: + """Get the log file path.""" + log_path = os.getenv("FLET_APP_CONSOLE") + if log_path: + return log_path + + import tempfile + + try: + test_path = "debug.log" + with open(test_path, "a"): + pass + return test_path + except (OSError, IOError): + pass + + return os.path.join(tempfile.gettempdir(), "{{ package_name }}_debug.log") + + +def _tail_file(filepath: str, num_lines: int) -> list[str]: + """Efficiently read the last N lines of a file.""" + try: + with open(filepath, "rb") as f: + f.seek(0, 2) + file_size = f.tell() + + if file_size == 0: + return [] + + buffer_size = min(8192, file_size) + lines: list[str] = [] + remaining = file_size + + while remaining > 0 and len(lines) <= num_lines: + read_size = min(buffer_size, remaining) + f.seek(remaining - read_size) + chunk = f.read(read_size) + remaining -= read_size + + text = chunk.decode("utf-8", errors="replace") + chunk_lines = text.splitlines(keepends=True) + + if lines and chunk_lines: + lines[0] = chunk_lines[-1] + lines[0] + chunk_lines = chunk_lines[:-1] + + lines = chunk_lines + lines + + return [line.strip() for line in lines[-num_lines:] if line.strip()] + except Exception: + return [] + + +def _parse_log_line(line: str) -> tuple[str, LogLevel, str]: + """ + Fast parse of log line. Returns (timestamp, level, message). + + Expected format: [timestamp] [LEVEL] ... - message + """ + try: + # Find first bracket pair for timestamp + if line.startswith("["): + end_ts = line.find("]") + if end_ts > 0: + timestamp = line[1:end_ts] + rest = line[end_ts + 1 :].lstrip() + + # Find level in second bracket + if rest.startswith("["): + end_lvl = rest.find("]") + if end_lvl > 0: + level_str = rest[1:end_lvl] + level = LogLevel.from_string(level_str) + + # Find message after " - " + msg_start = rest.find(" - ", end_lvl) + if msg_start > 0: + message = rest[msg_start + 3 :] + else: + message = rest[end_lvl + 1 :].lstrip() + + return timestamp, level, message + except Exception: + pass + + # Fallback: return line as-is + return datetime.now().strftime("%H:%M:%S"), LogLevel.INFO, line + + +def setup_logging( + level: int = logging.INFO, + format_string: str = "[{asctime}] [{levelname}] [{filename}:{funcName}:{lineno}] - {message}", + max_bytes: int = 256 * 1024, # 256 KB max (reduced) + backup_count: int = 1, +) -> logging.Logger: + """Setup file-based logging with automatic rotation for use with DebugConsole. + + Writes logs to the path specified by the ``FLET_APP_CONSOLE`` environment + variable, or to ``debug.log`` in the current directory, or to a temp file + as fallback. + + Args: + level: Logging level (default: ``logging.INFO``). + format_string: Log format string using ``{}``-style formatting. + max_bytes: Maximum log file size in bytes before rotation (default: 256 KB). + backup_count: Number of rotated backup files to keep (default: 1). + + Returns: + Configured logger instance. + + Example: + ```python + import {{ package_name }} as pkg + + logger = pkg.setup_logging() + logger.info("App started") + ``` + """ + log_path = get_log_path() + + file_handler = RotatingFileHandler( + log_path, + mode="a", + maxBytes=max_bytes, + backupCount=backup_count, + encoding="utf-8", + ) + file_handler.setFormatter(logging.Formatter(format_string, style="{")) + + console_handler = logging.StreamHandler() + console_handler.setFormatter(logging.Formatter(format_string, style="{")) + + logger = logging.getLogger() + logger.setLevel(level) + + for handler in logger.handlers[:]: + logger.removeHandler(handler) + + logger.addHandler(file_handler) + logger.addHandler(console_handler) + + return logging.getLogger(__name__) + + +class DebugConsole: + """Lightweight debug console for viewing application logs. + + Provides an ``icon`` (for AppBar) and a ``fab`` (floating action button) that + open a dialog showing application logs with color-coded levels and filtering. + + Reads log entries written by ``setup_logging()``. + + Args: + title: Dialog title (default: ``"Debug Console"``). + max_lines: Maximum number of log lines to display (default: 200). + + Example: + ```python + import {{ package_name }} as pkg + + logger = pkg.setup_logging() + debug = pkg.DebugConsole() + + # Use icon in AppBar + page.appbar = ft.AppBar( + title=ft.Text("My App"), + actions=[debug.icon], + ) + + # Or use FAB + page.floating_action_button = debug.fab + ``` + """ + + MAX_DISPLAY_LINES = 200 + """Maximum lines to display (performance limit).""" + + def __init__( + self, + title: str = "Debug Console", + max_lines: int = 200, + ): + self._title = title + self._max_lines = min(max_lines, self.MAX_DISPLAY_LINES) + self._page: Optional[ft.Page] = None + self._dialog: Optional[ft.AlertDialog] = None + self._log_column: Optional[ft.Column] = None + self._cached_lines: list[str] = [] + self._current_filter: Optional[LogLevel] = None + + # Simple icon without badge + self._icon = ft.IconButton( + icon=ft.Icons.BUG_REPORT_OUTLINED, + tooltip="Debug Console", + on_click=self._show_console, + ) + + # FAB + self._fab = ft.FloatingActionButton( + icon=ft.Icons.BUG_REPORT, + tooltip="Debug Console", + on_click=self._show_console, + bgcolor=ft.Colors.BLUE_GREY_700, + ) + + @property + def icon(self) -> ft.IconButton: + """Get the debug icon for use in AppBar actions.""" + return self._icon + + @property + def fab(self) -> ft.FloatingActionButton: + """Get the FAB for use as floating action button.""" + return self._fab + + def _read_logs(self) -> list[str]: + """Read logs from file (cached).""" + log_path = get_log_path() + if not os.path.exists(log_path): + return [] + return _tail_file(log_path, self._max_lines) + + def _create_log_text(self, line: str) -> ft.Text: + """Create a simple Text widget for a log line.""" + timestamp, level, message = _parse_log_line(line) + + # Format: [HH:MM:SS] [LEVEL] message + short_ts = timestamp.split()[-1] if " " in timestamp else timestamp + display = f"[{short_ts}] [{level.value[0][:4]}] {message}" + + return ft.Text( + display, + size=11, + color=level.color, + selectable=True, + no_wrap=False, + ) + + def _build_log_controls( + self, lines: list[str], filter_level: Optional[LogLevel] = None + ) -> list[ft.Text]: + """Build log text controls from lines.""" + controls = [] + + for line in lines: + if filter_level: + _, level, _ = _parse_log_line(line) + if filter_level == LogLevel.ERROR: + if level not in (LogLevel.ERROR, LogLevel.CRITICAL): + continue + elif level != filter_level: + continue + + controls.append(self._create_log_text(line)) + + return controls + + def _show_console(self, e): + """Show the console dialog.""" + if e.control.page: + self._page = e.control.page + + if not self._page: + return + + # Read and cache logs + self._cached_lines = self._read_logs() + self._current_filter = None + + # Build log controls + log_controls = self._build_log_controls(self._cached_lines) + + # Create scrollable column for logs + self._log_column = ft.Column( + controls=log_controls + if log_controls + else [ft.Text("No logs available", italic=True, color=ft.Colors.GREY_500)], + spacing=2, + scroll=ft.ScrollMode.AUTO, + auto_scroll=True, + expand=True, + ) + + # Create dialog + self._dialog = ft.AlertDialog( + title=ft.Row( + controls=[ + ft.Icon(ft.Icons.BUG_REPORT, color=ft.Colors.BLUE_600, size=20), + ft.Text(self._title, weight=ft.FontWeight.BOLD, size=16), + ft.Container(expand=True), + ft.Text( + f"{len(log_controls)} lines", + size=11, + color=ft.Colors.GREY_600, + ), + ], + spacing=8, + ), + content=ft.Container( + content=ft.Column( + controls=[ + # Filter buttons + ft.Row( + controls=[ + ft.TextButton("All", on_click=lambda _: self._apply_filter(None)), + ft.TextButton( + "Errors", + on_click=lambda _: self._apply_filter(LogLevel.ERROR), + style=ft.ButtonStyle(color=ft.Colors.RED_600), + ), + ft.TextButton( + "Warnings", + on_click=lambda _: self._apply_filter(LogLevel.WARNING), + style=ft.ButtonStyle(color=ft.Colors.AMBER_700), + ), + ], + spacing=0, + ), + ft.Divider(height=1), + # Log content + ft.Container( + content=self._log_column, + expand=True, + padding=8, + border=ft.border.all(1, ft.Colors.GREY_400), + border_radius=4, + ), + ], + expand=True, + spacing=4, + ), + width=500, + height=350, + ), + actions=[ + ft.TextButton("Clear", on_click=self._clear_logs), + ft.TextButton("Refresh", on_click=self._refresh_logs), + ft.TextButton("Close", on_click=self._close_dialog), + ], + actions_alignment=ft.MainAxisAlignment.END, + ) + + # Use Flet 0.80.x dialog API + self._page.show_dialog(self._dialog) + + def _apply_filter(self, level: Optional[LogLevel]): + """Apply filter to cached logs.""" + if not self._log_column or not self._page: + return + + self._current_filter = level + log_controls = self._build_log_controls(self._cached_lines, level) + + if log_controls: + self._log_column.controls = log_controls + else: + filter_name = level.value[0] if level else "matching" + self._log_column.controls = [ + ft.Text(f"No {filter_name} logs found", italic=True, color=ft.Colors.GREY_500) + ] + + self._page.update() + + def _clear_logs(self, e): + """Clear logs from file.""" + try: + log_path = get_log_path() + if os.path.exists(log_path): + with open(log_path, "w", encoding="utf-8") as f: + f.truncate(0) + except Exception: + pass + + self._cached_lines = [] + + if self._log_column: + self._log_column.controls = [ + ft.Text("Logs cleared", italic=True, color=ft.Colors.GREY_500) + ] + + if self._page: + self._page.update() + + def _refresh_logs(self, e): + """Refresh logs from file.""" + self._cached_lines = self._read_logs() + self._apply_filter(self._current_filter) + + def _close_dialog(self, e): + """Close the dialog.""" + if self._page: + self._page.pop_dialog() diff --git a/src/flet_pkg/templates/service/{{project_name}}/src/{{package_name}}/types.py.jinja b/src/flet_pkg/templates/service/{{project_name}}/src/{{package_name}}/types.py.jinja new file mode 100644 index 0000000..7491fcf --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/src/{{package_name}}/types.py.jinja @@ -0,0 +1,12 @@ +from dataclasses import dataclass +from typing import Optional +import flet as ft + + +@dataclass +class {{ control_name }}ErrorEvent(ft.Event["{{ control_name }}"]): + """Event fired when an error occurs.""" + + method: Optional[str] = None + message: Optional[str] = None + stack_trace: Optional[str] = None diff --git a/src/flet_pkg/templates/service/{{project_name}}/src/{{package_name}}/{{control_name_snake}}.py.jinja b/src/flet_pkg/templates/service/{{project_name}}/src/{{package_name}}/{{control_name_snake}}.py.jinja new file mode 100644 index 0000000..762133d --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/src/{{package_name}}/{{control_name_snake}}.py.jinja @@ -0,0 +1,22 @@ +from typing import Optional +import flet as ft + +from {{ package_name }}.types import {{ control_name }}ErrorEvent + + +@ft.control("{{ control_name }}") +class {{ control_name }}(ft.Service): + """{{ description }}""" + + # TODO: Add your properties here + # example_property: str = "" + + on_error: Optional[ft.EventHandler[{{ control_name }}ErrorEvent]] = None + """Called when an error occurs in the SDK.""" + + # TODO: Add your event handlers here + # on_event: Optional[ft.EventHandler["{{ control_name }}Event"]] = None + + async def example_method(self, value: str) -> None: + """Example method that calls Flutter side.""" + await self._invoke_method("example_method", {"value": value}) diff --git a/src/flet_pkg/templates/service/{{project_name}}/tests/test_{{package_name}}.py.jinja b/src/flet_pkg/templates/service/{{project_name}}/tests/test_{{package_name}}.py.jinja new file mode 100644 index 0000000..389d5b1 --- /dev/null +++ b/src/flet_pkg/templates/service/{{project_name}}/tests/test_{{package_name}}.py.jinja @@ -0,0 +1,12 @@ +from {{ package_name }} import {{ control_name }} + + +def test_import(): + """Test that the {{ control_name }} class can be imported.""" + assert {{ control_name }} is not None + + +def test_instance(): + """Test that a {{ control_name }} instance can be created.""" + instance = {{ control_name }}() + assert instance is not None diff --git a/src/flet_pkg/templates/ui_control/template.yaml b/src/flet_pkg/templates/ui_control/template.yaml new file mode 100644 index 0000000..552f4c8 --- /dev/null +++ b/src/flet_pkg/templates/ui_control/template.yaml @@ -0,0 +1,2 @@ +name: ui_control +description: Flet UI Control extension (visual widget) diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/CHANGELOG.md.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/CHANGELOG.md.jinja new file mode 100644 index 0000000..7a5b472 --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/CHANGELOG.md.jinja @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.0 + +- Initial release of {{ project_name }}. diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/LICENSE.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/LICENSE.jinja new file mode 100644 index 0000000..9fc4e61 --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/LICENSE.jinja @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) {{ author }} + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/README.md.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/README.md.jinja new file mode 100644 index 0000000..9e1124c --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/README.md.jinja @@ -0,0 +1,70 @@ +# {{ project_name }} + +{{ description }} + +## Installation + +```bash +# Using UV (recommended) +uv add {{ project_name }} + +# Using pip +pip install {{ project_name }} +``` + +## Usage + +```python +{% if include_console -%} +import logging + +{% endif -%} +import flet as ft +from {{ package_name }} import {{ control_name }} +{%- if include_console %} +from {{ package_name }}.console import DebugConsole, setup_logging + +logger = setup_logging(level=logging.DEBUG) +{%- endif %} + + +async def main(page: ft.Page): + page.title = "{{ control_name }} Example" +{%- if include_console %} + + debug = DebugConsole() + page.appbar = ft.AppBar( + title=ft.Text("{{ control_name }} Example"), + actions=[debug.icon], + ) + logger.info("App started") +{%- endif %} + + page.add( + {{ control_name }}( +{%- if include_console %} + on_error=lambda e: logger.error(f"Error: {e.method}: {e.message}"), +{%- else %} + on_error=lambda e: print(f"Error: {e.method}: {e.message}"), +{%- endif %} + ), + ) + + +if __name__ == "__main__": + ft.run(main) +``` + +## Development + +```bash +# Using UV (recommended) +uv sync --dev + +# Using pip +pip install -e ".[dev]" +``` + +## License + +MIT diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/about_pkg_flet.md.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/about_pkg_flet.md.jinja new file mode 100644 index 0000000..a3b8680 --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/about_pkg_flet.md.jinja @@ -0,0 +1,585 @@ +# About Package for Flet + +> **Auto-generated by [flet-pkg](https://github.com/nicholasgasior/flet-pkg)** +> as part of **{{ project_name }}** — wrapping the Flutter package +> `{{ flutter_package }}`. +> +> This guide explains how Flet extensions work, how to adjust and extend the +> generated code, and how to build, test, and publish your extension. +> Keep this file as a reference throughout development. + +--- + +## Part 1 — Concepts (Beginner) + +### The Simple Idea + +Think of a Flet extension like a **remote control**: + +- **Python** is the remote — it sends commands and receives feedback. +- **Dart** is the TV — it executes the commands using the native Flutter SDK. +- **Flet Engine** is the infrared signal — it carries messages between them. + +You write Python, the user writes Python, and under the hood Flet handles all +the communication with Dart. Nobody needs to learn Dart to _use_ the extension. + +### What is a Flet Extension? + +A Flet extension wraps a native Flutter package so that Python developers can +use it directly from Flet. The extension ships two layers of code: + +| Layer | Language | Role | +|--------|----------|------------------------------------------------------| +| Python | Python | Public API — controls, properties, events, types | +| Dart | Dart | Bridge — receives commands and calls the Flutter SDK | + +### Extension Types + +Flet supports two extension types: + +| Type | Base class | Factory method | Use case | +|----------------|--------------------|-------------------|-----------------------------------| +| **Service** | `ft.Service` | `createService` | Background logic, no UI | +| **UI Control** | `ft.LayoutControl` | `createWidget` | Visual widget rendered on screen | + +**{{ project_name }}** uses the **UI Control** type (`ft.LayoutControl`). + +### How Communication Works (Overview) + +There are three ways Python and Dart talk to each other: + +1. **Commands** (Python → Dart) — Python calls a method, Dart executes it +2. **Events** (Dart → Python) — Dart sends notifications back to Python +3. **Properties** (Python → Dart) — Python sets a value, Dart reads it on next update + +That's it. Every feature in your extension is built from these three patterns. + +--- + +## Part 2 — Implementation (Intermediate) + +### Directory Layout + +``` +{{ project_name }}/ +├── pyproject.toml # Python package config +├── README.md +├── about_pkg_flet.md # This guide +├── src/ +│ ├── {{ package_name }}/ # Python package +│ │ ├── __init__.py # Public exports +│ │ ├── {{ control_name_snake }}.py # Control class +│ │ └── types.py # Event dataclasses +│ └── flutter/ +│ └── {{ package_name }}/ # Dart package +│ ├── pubspec.yaml # Flutter dependencies +│ └── lib/ +│ ├── {{ package_name }}.dart # Barrel export +│ └── src/ +│ ├── extension.dart # Factory registration +│ └── {{ control_name_snake }}_control.dart # Dart widget +├── examples/ +│ └── {{ package_name }}_example/ +│ └── src/main.py # Example app +├── scripts/ +│ ├── flet_log.py # Android logcat viewer (Python) +│ └── flet_log.sh # Android logcat viewer (Bash) +└── tests/ + └── test_{{ package_name }}.py +``` + +### Python: Control Class + +```python +# src/{{ package_name }}/{{ control_name_snake }}.py +from typing import Optional +import flet as ft +from {{ package_name }}.types import {{ control_name }}ErrorEvent + +@ft.control("{{ control_name }}") +class {{ control_name }}(ft.LayoutControl): + """{{ description }}""" + + # Visual properties — synced to Dart on page.update() + # color: Optional[ft.ColorValue] = None + # size: float = 100.0 + + on_error: Optional[ft.EventHandler[{{ control_name }}ErrorEvent]] = None +``` + +Key points: +- `@ft.control("{{ control_name }}")` registers the control type name +- `ft.LayoutControl` is the base class for visual widgets +- Properties are read on the Dart side via `control.getXxx()` accessors +- Event handlers are typed `Optional[ft.EventHandler[EventType]]` + +### Python: Event Types + +```python +# src/{{ package_name }}/types.py +from dataclasses import dataclass +from typing import Optional +import flet as ft + +@dataclass +class {{ control_name }}ErrorEvent(ft.Event["{{ control_name }}"]): + """Event fired when an error occurs.""" + method: Optional[str] = None + message: Optional[str] = None + stack_trace: Optional[str] = None +``` + +Each event is a `@dataclass` extending `ft.Event[ControlType]`. Field names +must match the keys passed to `triggerEvent()` on the Dart side. + +### Python: `__init__.py` + +```python +# src/{{ package_name }}/__init__.py +from {{ package_name }}.{{ control_name_snake }} import {{ control_name }} +from {{ package_name }}.types import {{ control_name }}ErrorEvent + +__all__ = ["{{ control_name }}", "{{ control_name }}ErrorEvent"] +``` + +### Dart: UI Control Implementation + +```dart +// src/flutter/{{ package_name }}/lib/src/{{ control_name_snake }}_control.dart +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +class {{ control_name }}Control extends StatelessWidget { + final Control control; + const {{ control_name }}Control({super.key, required this.control}); + + @override + Widget build(BuildContext context) { + // Replace with your actual Flutter widget + Widget myWidget = Container( + color: control.getColor("color", context), + child: const Center( + child: Text("{{ control_name }}"), + ), + ); + return LayoutControl(control: control, child: myWidget); + } +} +``` + +Key points: +- The widget reads properties from `control.getXxx()` accessors +- Wrap the final widget with `LayoutControl(control: control, child: ...)` to + enable Flet layout features (width, height, alignment, etc.) +- Use `control.triggerEvent(name, data)` to send events back to Python + +### Dart: `extension.dart` + +```dart +// src/flutter/{{ package_name }}/lib/src/extension.dart +import 'package:flet/flet.dart'; +import 'package:flutter/widgets.dart'; +import '{{ control_name_snake }}_control.dart'; + +class Extension extends FletExtension { + @override + Widget? createWidget(Key? key, Control control) { + switch (control.type) { + case "{{ control_name }}": + return {{ control_name }}Control(control: control); + default: + return null; + } + } +} +``` + +### Dart: Barrel Export + +```dart +// src/flutter/{{ package_name }}/lib/{{ package_name }}.dart +library {{ package_name }}; + +export 'src/extension.dart' show Extension; +``` + +### Configuration: `pubspec.yaml` + +```yaml +name: {{ package_name }} +description: "{{ description }}" +version: 0.1.0 + +environment: + sdk: ^3.5.0 + flutter: ">=3.29.0" + +dependencies: + flutter: + sdk: flutter + flet: ^0.80.5 + {{ flutter_package }}: # Add version constraint +``` + +### Configuration: `pyproject.toml` + +```toml +[project] +name = "{{ project_name }}" +version = "0.1.0" +description = "{{ description }}" +requires-python = ">=3.10" +dependencies = ["flet>=0.80.0"] + +[tool.setuptools.package-data] +"flutter.{{ package_name }}" = ["**/*"] + +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" +``` + +The `package-data` section is critical — it tells setuptools to include the +Dart/Flutter code in the built wheel so Flet can find it at runtime. + +--- + +## Part 3 — Internals (Advanced) + +### Communication Bridge + +``` + Python (your app) Dart (Flutter) + ┌──────────────────┐ ┌──────────────────┐ + │ {{ control_name }} │ ── invoke ──▶ │ {{ control_name }}Control │ + │ │ │ │ + │ on_xxx handler │ ◀── event ─── │ triggerEvent() │ + │ │ │ │ + │ property = val │ ── sync ──▶ │ control.getXxx()│ + └──────────────────┘ └──────────────────┘ + ▲ │ + │ Flet Engine (platform channel) │ + └──────────────────────────────────────────┘ +``` + +### Method Invocation Flow + +When Python calls a method, this is the exact sequence: + +``` +Python Dart +────── ──── +1. await self._invoke_method( 2. _onInvokeMethod(methodName, args) + "do_something", 3. switch (methodName) → _doSomething(args) + {"key": value} 4. Call Flutter SDK + ) 5. Return result / throw +6. Receives return value + or raises exception +``` + +The method name string `"do_something"` must be **identical** on both sides. +Python sends it via `_invoke_method`, Dart receives it in the `switch` +statement. A typo on either side silently fails. + +### Event Flow + +When Dart needs to notify Python (callbacks, state changes, errors): + +``` +Dart Python +──── ────── +1. SDK fires callback 4. on_xxx handler receives +2. Build event data map typed dataclass event +3. control.triggerEvent( 5. Developer processes event + "event_name", data) +``` + +Events arrive as typed dataclasses extending `ft.Event`. For example, +`{{ control_name }}ErrorEvent` carries `method`, `message`, and `stack_trace` fields. + +The event name `"error"` maps to the handler `on_error`. The mapping convention +is: `triggerEvent("some_event", ...)` → `on_some_event` handler in Python. + +### Property Synchronisation + +Properties declared on the Python control class are automatically synced to +the Dart side when `page.update()` is called: + +- **Python** — set `self.some_prop = "value"` +- **Dart** — read `control.getString("some_prop")` + +Common Dart accessors: `getString`, `getInt`, `getDouble`, `getBool`, +`getColor`, `getList`. + +If a property change doesn't seem to reach Dart, make sure `page.update()` +is being called after setting the value. + +### Registration + +Every Flet extension has an `extension.dart` that registers controls with the +engine via a factory method: + +```dart +class Extension extends FletExtension { + @override + Widget? createWidget(Key? key, Control control) { + switch (control.type) { + case "{{ control_name }}": + return {{ control_name }}Control(control: control); + default: + return null; + } + } +} +``` + +The `control.type` string must exactly match the string in +`@ft.control("{{ control_name }}")` on the Python side. + +### Type Serialisation + +| Python | Dart | Notes | +|---------------------|--------------------|------------------------------| +| `str` | `String` | | +| `int` | `int` | | +| `float` | `double` | | +| `bool` | `bool` | | +| `list` | `List` | JSON array | +| `dict` | `Map` | JSON object | +| `None` | `null` | | +| `ft.ColorValue` | `Color` | `control.getColor()` | +| `enum` (`.value`) | `String` | Sent as string value | +| `@dataclass` | `Map` | Serialised to/from JSON map | + +--- + +## Part 4 — Install, Test, and Publish (Practical) + +### Installing the Extension + +```bash +# From PyPI (after publishing) +pip install {{ project_name }} + +# From local source (development) +pip install -e . +# or +uv add --editable . +``` + +### Using in Your Flet App + +```python +import flet as ft +from {{ package_name }} import {{ control_name }} + +def main(page: ft.Page): + page.add( + {{ control_name }}( + width=200, + height=200, + ), + ) + +ft.run(main) +``` + +### Building for All Platforms + +```bash +# Desktop +flet build macos +flet build windows +flet build linux + +# Mobile +flet build apk # Android APK +flet build aab # Android App Bundle +flet build ipa # iOS + +# Web +flet build web +``` + +### Local Testing with Example App + +```bash +cd examples/{{ package_name }}_example +pip install -e ../../ # Install extension from source +flet run src/main.py +``` + +Or with path dependency in the example's `pyproject.toml`: + +```toml +[project] +dependencies = [ + "{{ project_name }} @ file:///${PROJECT_ROOT}/../..", +] +``` + +### Testing with a Built Wheel + +You can build a `.whl` (wheel) file and reference it directly as a dependency. +This is useful for testing the exact artifact that will be published to PyPI, +or for sharing pre-built packages without publishing. + +**Step 1 — Build the wheel:** + +```bash +# Using uv (recommended) +uv build + +# Or using standard Python build +python -m build +``` + +This creates a `dist/` directory with files like: + +``` +dist/ +├── {{ project_name }}-0.1.0-py3-none-any.whl +└── {{ project_name }}-0.1.0.tar.gz +``` + +**Step 2 — Reference the `.whl` in another project's `pyproject.toml`:** + +```toml +[project] +dependencies = [ + "flet>=0.80.5", + "{{ project_name }} @ file:///absolute/path/to/{{ project_name }}/dist/{{ package_name }}-0.1.0-py3-none-any.whl", +] +``` + +Then sync the project: + +```bash +uv sync +# or +pip install -e . +``` + +This installs the exact wheel build, including all Dart/Flutter code bundled +inside. It behaves identically to a PyPI install, so `flet build` will find +the Flutter package correctly. + +### Development Dependencies + +For development, use an editable install with path or git dependency: + +```toml +# Path dependency (local dev — installs from source, reflects changes) +dependencies = [ + "{{ project_name }} @ file:///path/to/{{ project_name }}", +] + +# Wheel dependency (local dev — installs a specific build snapshot) +dependencies = [ + "{{ project_name }} @ file:///path/to/{{ project_name }}/dist/{{ package_name }}-0.1.0-py3-none-any.whl", +] + +# Git dependency +dependencies = [ + "{{ project_name }} @ git+https://github.com/youruser/{{ project_name }}.git", +] +``` + +### Publishing to PyPI + +```bash +# Build wheel and sdist +uv build +# or: python -m build + +# Publish with uv (recommended — builds + uploads in one step) +uv publish +# Prompts: Enter username ('__token__' if using a token): + +# Or publish with twine +twine upload dist/* + +# Or upload to Test PyPI first +uv publish --publish-url https://test.pypi.org/legacy/ +# or: twine upload --repository testpypi dist/* +``` + +After publishing, users install with: + +```bash +pip install {{ project_name }} +``` + +--- + +## Debugging with Android LogCat + +The `scripts/` directory includes two logcat viewers (Python and Bash) that +filter Android logs for Flet/Flutter-related output. They auto-detect the +focused app and colour-code log levels like Android Studio. + +```bash +# Python version +python scripts/flet_log.py + +# Bash version +./scripts/flet_log.sh + +# Add an extra filter for your Flutter package +python scripts/flet_log.py "{{ flutter_package }}" +./scripts/flet_log.sh "{{ flutter_package }}" +``` + +**Requirements:** `adb` must be installed and your Android device connected +via USB or Wi-Fi debugging. + +**Default filter:** `flutter | python | Error | Exception | Traceback` + +The scripts automatically: +- Detect which app is in the foreground +- Filter logs by the app's PID +- Re-attach when the app restarts or focus changes +- Format output with colour-coded log levels (V/D/I/W/E/F) + +### Printing to LogCat from Dart + +Use `debugPrint()` or `print()` in your Dart code to send messages to LogCat. +Both appear in the filtered output of `flet_log.py` / `flet_log.sh`: + +```dart +import 'package:flutter/foundation.dart'; + +// Preferred — truncates long output to avoid log overflow +debugPrint("{{ control_name }}Control: building with color=$color"); + +// Also works — simpler but no truncation protection +print("{{ control_name }}Control: something happened"); +``` + +`debugPrint` is recommended over `print` because it throttles output and +truncates very long strings, preventing log buffer overflow on Android. + +--- + +## Debugging Checklist + +If something doesn't work, walk through this list: + +- [ ] **Control name match** — `@ft.control("{{ control_name }}")` in Python matches + `case "{{ control_name }}"` in Dart's `extension.dart` +- [ ] **Method names match** — the string in `_invoke_method("method_name", ...)` + matches exactly in the Dart `switch` statement +- [ ] **Event names match** — `control.triggerEvent("event_name", ...)` in Dart + maps to `on_event_name` handler in Python +- [ ] **`page.update()` called** — property changes only reach Dart after calling + `page.update()` (or `await page.update_async()`) +- [ ] **Event dataclass fields match** — field names in the Python `@dataclass` + match the keys in the Dart `triggerEvent` data map +- [ ] **Package-data included** — `pyproject.toml` has `[tool.setuptools.package-data]` + with `"flutter.{{ package_name }}" = ["**/*"]` +- [ ] **Extension registered** — `extension.dart` exists and the barrel file + (`{{ package_name }}.dart`) exports it +- [ ] **Flutter dependency added** — `pubspec.yaml` lists `{{ flutter_package }}` + under `dependencies` +- [ ] **Rebuild after Dart changes** — Dart code changes require `flet build` + again; only Python changes are hot-reloaded diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/docs/api-reference.md.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/docs/api-reference.md.jinja new file mode 100644 index 0000000..fc7a944 --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/docs/api-reference.md.jinja @@ -0,0 +1,73 @@ +# API Reference + +## {{ control_name }} + +`{{ control_name }}` is a Flet UI Control extension based on `ft.LayoutControl`. + +### Import + +```python +from {{ package_name }} import {{ control_name }} +``` + +### Properties + +Since `{{ control_name }}` extends `ft.LayoutControl`, it inherits all standard layout properties: + +| Property | Type | Description | +|----------|------|-------------| +| `width` | `float` | Width of the control | +| `height` | `float` | Height of the control | +| `margin` | `Margin` | Margin around the control | +| `padding` | `Padding` | Padding inside the control | +| `align` | `Alignment` | Alignment of the control | +| `visible` | `bool` | Whether the control is visible | + +TODO: Document custom properties here. + +### Events + +TODO: Document event handlers here. + +### Example + +```python +{% if include_console -%} +import logging + +{% endif -%} +import flet as ft +from {{ package_name }} import {{ control_name }} +{%- if include_console %} +from {{ package_name }}.console import DebugConsole, setup_logging + +logger = setup_logging(level=logging.DEBUG) +{%- endif %} + + +async def main(page: ft.Page): + page.title = "{{ control_name }} Example" +{%- if include_console %} + + debug = DebugConsole() + page.appbar = ft.AppBar( + title=ft.Text("{{ control_name }} Example"), + actions=[debug.icon], + ) + logger.info("App started") +{%- endif %} + + page.add( + {{ control_name }}( +{%- if include_console %} + on_error=lambda e: logger.error(f"Error: {e.method}: {e.message}"), +{%- else %} + on_error=lambda e: print(f"Error: {e.method}: {e.message}"), +{%- endif %} + ), + ) + + +if __name__ == "__main__": + ft.run(main) +``` diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/docs/changelog.md.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/docs/changelog.md.jinja new file mode 100644 index 0000000..7a5b472 --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/docs/changelog.md.jinja @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.0 + +- Initial release of {{ project_name }}. diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/docs/examples.md.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/docs/examples.md.jinja new file mode 100644 index 0000000..c687bce --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/docs/examples.md.jinja @@ -0,0 +1,48 @@ +# Examples + +## Basic Usage + +```python +{% if include_console -%} +import logging + +{% endif -%} +import flet as ft +from {{ package_name }} import {{ control_name }} +{%- if include_console %} +from {{ package_name }}.console import DebugConsole, setup_logging + +logger = setup_logging(level=logging.DEBUG) +{%- endif %} + + +async def main(page: ft.Page): + page.title = "{{ control_name }} Example" +{%- if include_console %} + + debug = DebugConsole() + page.appbar = ft.AppBar( + title=ft.Text("{{ control_name }} Example"), + actions=[debug.icon], + ) + logger.info("App started") +{%- endif %} + + page.add( + {{ control_name }}( +{%- if include_console %} + on_error=lambda e: logger.error(f"Error: {e.method}: {e.message}"), +{%- else %} + on_error=lambda e: print(f"Error: {e.method}: {e.message}"), +{%- endif %} + ), + ) + + +if __name__ == "__main__": + ft.run(main) +``` + +## More Examples + +TODO: Add more usage examples here. diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/docs/getting-started.md.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/docs/getting-started.md.jinja new file mode 100644 index 0000000..478038d --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/docs/getting-started.md.jinja @@ -0,0 +1,60 @@ +# Getting Started + +## Installation + +```bash +# Using UV (recommended) +uv add {{ project_name }} + +# Using pip +pip install {{ project_name }} +``` + +## Basic Usage + +```python +{% if include_console -%} +import logging + +{% endif -%} +import flet as ft +from {{ package_name }} import {{ control_name }} +{%- if include_console %} +from {{ package_name }}.console import DebugConsole, setup_logging + +logger = setup_logging(level=logging.DEBUG) +{%- endif %} + + +async def main(page: ft.Page): + page.title = "{{ control_name }} Example" +{%- if include_console %} + + debug = DebugConsole() + page.appbar = ft.AppBar( + title=ft.Text("{{ control_name }} Example"), + actions=[debug.icon], + ) + logger.info("App started") +{%- endif %} + + page.add( + {{ control_name }}( +{%- if include_console %} + on_error=lambda e: logger.error(f"Error: {e.method}: {e.message}"), +{%- else %} + on_error=lambda e: print(f"Error: {e.method}: {e.message}"), +{%- endif %} + ), + ) + + +if __name__ == "__main__": + ft.run(main) +``` + +## Next Steps + +- Check the [API Reference](api-reference.md) for all available properties. +- See the [Examples](examples.md) for more usage patterns. +- See the [Changelog](changelog.md) for version history. diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/docs/index.md.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/docs/index.md.jinja new file mode 100644 index 0000000..7c9d837 --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/docs/index.md.jinja @@ -0,0 +1,58 @@ +# {{ project_name }} + +{{ description }} + +## Overview + +`{{ project_name }}` is a Flet UI Control extension that provides the `{{ control_name }}` visual widget for your Flet applications. + +## Quick Start + +```bash +# Using UV (recommended) +uv add {{ project_name }} + +# Using pip +pip install {{ project_name }} +``` + +```python +{% if include_console -%} +import logging + +{% endif -%} +import flet as ft +from {{ package_name }} import {{ control_name }} +{%- if include_console %} +from {{ package_name }}.console import DebugConsole, setup_logging + +logger = setup_logging(level=logging.DEBUG) +{%- endif %} + + +async def main(page: ft.Page): + page.title = "{{ control_name }} Example" +{%- if include_console %} + + debug = DebugConsole() + page.appbar = ft.AppBar( + title=ft.Text("{{ control_name }} Example"), + actions=[debug.icon], + ) + logger.info("App started") +{%- endif %} + + page.add( + {{ control_name }}( +{%- if include_console %} + on_error=lambda e: logger.error(f"Error: {e.method}: {e.message}"), +{%- else %} + on_error=lambda e: print(f"Error: {e.method}: {e.message}"), +{%- endif %} + ), + ) + + +if __name__ == "__main__": + ft.run(main) +``` diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/examples/{{package_name}}_example/pyproject.toml.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/examples/{{package_name}}_example/pyproject.toml.jinja new file mode 100644 index 0000000..9bcbcc2 --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/examples/{{package_name}}_example/pyproject.toml.jinja @@ -0,0 +1,19 @@ +[project] +name = "{{ package_name }}-example" +version = "0.1.0" +description = "Example app for {{ project_name }}" +requires-python = ">=3.10" +dependencies = [ + "flet>=0.80.0", + "{{ project_name }}>=0.1.0", +] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.flet.app] +path = "src" + +[tool.flet.dependencies] +{{ project_name }} = { path = "../../" } diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/examples/{{package_name}}_example/src/main.py.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/examples/{{package_name}}_example/src/main.py.jinja new file mode 100644 index 0000000..a933cc3 --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/examples/{{package_name}}_example/src/main.py.jinja @@ -0,0 +1,38 @@ +{% if include_console -%} +import logging + +{% endif -%} +import flet as ft +from {{ package_name }} import {{ control_name }} +{%- if include_console %} +from {{ package_name }}.console import DebugConsole, setup_logging + +logger = setup_logging(level=logging.DEBUG) +{%- endif %} + + +async def main(page: ft.Page): + page.title = "{{ control_name }} Example" +{%- if include_console %} + + debug = DebugConsole() + page.appbar = ft.AppBar( + title=ft.Text("{{ control_name }} Example"), + actions=[debug.icon], + ) + logger.info("App started") +{%- endif %} + + page.add( + {{ control_name }}( +{%- if include_console %} + on_error=lambda e: logger.error(f"Error: {e.method}: {e.message}"), +{%- else %} + on_error=lambda e: print(f"Error: {e.method}: {e.message}"), +{%- endif %} + ), + ) + + +if __name__ == "__main__": + ft.run(main) diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/mkdocs.yml.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/mkdocs.yml.jinja new file mode 100644 index 0000000..b6524cf --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/mkdocs.yml.jinja @@ -0,0 +1,25 @@ +site_name: {{ project_name }} +site_description: {{ description }} + +theme: + name: material + palette: + - scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + +nav: + - Home: index.md + - Getting Started: getting-started.md + - API Reference: api-reference.md + - Examples: examples.md + - Changelog: changelog.md diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/pyproject.toml.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/pyproject.toml.jinja new file mode 100644 index 0000000..11cc386 --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/pyproject.toml.jinja @@ -0,0 +1,28 @@ +[project] +name = "{{ project_name }}" +version = "0.1.0" +description = "{{ description }}" +readme = "README.md" +requires-python = ">=3.10" +license = { text = "MIT" } +authors = [ + { name = "{{ author }}" }, +] +dependencies = [ + "flet>=0.80.0", +] + +[project.urls] +Homepage = "https://github.com/{{ author }}/{{ project_name }}" +Documentation = "https://github.com/{{ author }}/{{ project_name }}" +Repository = "https://github.com/{{ author }}/{{ project_name }}" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +"flutter.{{ package_name }}" = ["**/*"] + +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/scripts/flet_log.py b/src/flet_pkg/templates/ui_control/{{project_name}}/scripts/flet_log.py new file mode 100644 index 0000000..1917989 --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/scripts/flet_log.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +""" +Flet Logcat - Android Studio Style + +Author: Bruno Brown (https://github.com/BrunoBrown) + +A Python script for monitoring Android logcat with Flet/Flutter app filtering. +Automatically detects the focused app and filters relevant logs. + +Usage: python flet_log.py [extra_filter] +Example: python flet_log.py "your_flutter_package" +""" + +import os +import re +import subprocess +import sys +import threading +import time +from typing import Optional + +# Colors by log level (Android Studio style) +COLORS = { + "V": "\033[0;37m", # Verbose - Light gray + "D": "\033[0;36m", # Debug - Cyan + "I": "\033[0;32m", # Info - Green + "W": "\033[0;33m", # Warning - Yellow + "E": "\033[0;31m", # Error - Red + "F": "\033[1;31m", # Fatal - Bold red + "A": "\033[1;31m", # Assert - Bold red +} +RESET = "\033[0m" +BOLD = "\033[1m" +DIM = "\033[2m" + +# Default filter +DEFAULT_FILTER = r"flutter|python|Error|Exception|Traceback" + +# Global state +current_process: Optional[subprocess.Popen] = None +stop_event = threading.Event() + + +def run_cmd(cmd: str) -> str: + """Run a shell command and return output.""" + result = subprocess.run( + cmd, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + ) + return result.stdout.strip() + + +def get_current_package() -> Optional[str]: + """Get the currently focused app package name.""" + output = run_cmd("adb shell dumpsys activity activities 2>/dev/null") + for line in output.splitlines(): + if "mResumedActivity" in line or "mFocusedActivity" in line: + match = re.search(r"[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z0-9_]+)+", line) + if match: + return match.group(0) + return None + + +def get_pid(package: str) -> Optional[str]: + """Get the PID of a package.""" + pid = run_cmd(f"adb shell pidof {package} 2>/dev/null") + return pid.split()[0] if pid else None + + +def format_log_line(line: str) -> Optional[str]: + """Format a log line in Android Studio style.""" + # Format: MM-DD HH:MM:SS.mmm PID TID LEVEL TAG: MESSAGE + pattern = ( + r"^(\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2}\.\d+)\s+" + r"(\d+)\s+(\d+)\s+([VDIWEFA])\s+([^:]+):\s+(.*)$" + ) + match = re.match(pattern, line) + + if match: + date, time_str, pid, tid, level, tag, msg = match.groups() + color = COLORS.get(level, RESET) + # Format: TIME PID-TID TAG LEVEL: MESSAGE + return ( + f"{DIM}{time_str}{RESET} {DIM}{pid:>5}-{tid:<5}{RESET} " + f"{color}{tag:<20} {level}: {msg}{RESET}" + ) + else: + # Unformatted line (continuation or different format) + return f"{DIM}{line}{RESET}" if line.strip() else None + + +def print_header(pkg: str, pid: Optional[str]) -> None: + """Print app header.""" + print() + print(f"{BOLD}━━━ {pkg} {DIM}PID: {pid or '?'}{RESET}") + print() + + +def print_usage(extra_filter: Optional[str]) -> None: + """Print usage instructions.""" + os.system("clear" if os.name == "posix" else "cls") + print(f"{BOLD}Logcat{RESET} {DIM}| Flet/Flutter{RESET}") + print() + print(f"{DIM}Usage: python flet_log.py [extra_filter]{RESET}") + print(f'{DIM}Example: python flet_log.py "your_flutter_package"{RESET}') + print() + print(f"{DIM}Default filter: flutter, python, Error, Exception, Traceback{RESET}") + if extra_filter: + print(f"{DIM}Extra filter: {extra_filter}{RESET}") + print(f"{DIM}Press Ctrl+C to exit{RESET}") + + +def logcat_thread(pid: Optional[str], pkg: str, filter_pattern: str) -> None: + """Thread to read and display logcat output.""" + global current_process + + try: + if pid: + cmd = ["adb", "logcat", "--pid", pid, "-v", "threadtime"] + else: + cmd = ["adb", "logcat", "-v", "threadtime"] + + current_process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + bufsize=1, + ) + + # Compile filter pattern + if pid: + pattern = re.compile(filter_pattern, re.IGNORECASE) + else: + pattern = re.compile(f"{pkg}|{filter_pattern}", re.IGNORECASE) + + if current_process.stdout is None: + return + + for line in current_process.stdout: + if stop_event.is_set(): + break + + line = line.rstrip() + if pattern.search(line): + formatted = format_log_line(line) + if formatted: + print(formatted) + + except Exception: + pass + finally: + if current_process: + current_process.terminate() + current_process = None + + +def main() -> None: + """Main entry point.""" + global current_process + + # Get extra filter from command line + extra_filter = sys.argv[1] if len(sys.argv) > 1 else None + + # Build filter pattern + filter_pattern = DEFAULT_FILTER + if extra_filter: + filter_pattern = f"{filter_pattern}|{extra_filter}" + + # Print usage + print_usage(extra_filter) + + current_pkg = "" + current_pid = "" + logcat_thread_handle: Optional[threading.Thread] = None + + try: + while True: + pkg = get_current_package() + + if not pkg: + time.sleep(1) + continue + + pid = get_pid(pkg) + + if pkg != current_pkg or pid != current_pid: + # Stop current logcat + stop_event.set() + if current_process: + current_process.terminate() + if logcat_thread_handle and logcat_thread_handle.is_alive(): + logcat_thread_handle.join(timeout=1) + + stop_event.clear() + current_pkg = pkg + current_pid = pid or "" + + print_header(pkg, pid) + + # Clear logcat + run_cmd("adb logcat -c 2>/dev/null") + + # Start new logcat thread + logcat_thread_handle = threading.Thread( + target=logcat_thread, + args=(pid, pkg, filter_pattern), + daemon=True, + ) + logcat_thread_handle.start() + + time.sleep(1) + + except KeyboardInterrupt: + pass + finally: + print() + print(f"{DIM}Stopped{RESET}") + stop_event.set() + if current_process: + current_process.terminate() + + +if __name__ == "__main__": + main() diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/scripts/flet_log.sh b/src/flet_pkg/templates/ui_control/{{project_name}}/scripts/flet_log.sh new file mode 100755 index 0000000..b90f869 --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/scripts/flet_log.sh @@ -0,0 +1,128 @@ +#!/bin/bash + +# ══════════════════════════════════════════════════════════════════════════════ +# Flet Logcat - Android Studio Style +# Author: Bruno Brown (https://github.com/BrunoBrown) +# ══════════════════════════════════════════════════════════════════════════════ +# Usage: ./flet_log.sh [filter] +# Example: ./flet_log.sh "your_flutter_package" +# ══════════════════════════════════════════════════════════════════════════════ + +# Colors by log level (Android Studio style) +declare -A COLORS=( + [V]="\033[0;37m" # Verbose - Light gray + [D]="\033[0;36m" # Debug - Cyan + [I]="\033[0;32m" # Info - Green + [W]="\033[0;33m" # Warning - Yellow + [E]="\033[0;31m" # Error - Red + [F]="\033[1;31m" # Fatal - Bold red + [A]="\033[1;31m" # Assert - Bold red +) +RESET="\033[0m" +BOLD="\033[1m" +DIM="\033[2m" + +EXTRA_FILTER="${1:-}" +FILTER="flutter|python|Error|Exception|Traceback" +[[ -n "$EXTRA_FILTER" ]] && FILTER="$FILTER|$EXTRA_FILTER" + +get_package() { + adb shell dumpsys activity activities 2>/dev/null | \ + grep -E "mResumedActivity|mFocusedActivity" | \ + head -1 | \ + grep -oE '[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z0-9_]+)+' | \ + head -1 +} + +get_pid() { + adb shell pidof "$1" 2>/dev/null | tr -d '\r\n' | awk '{print $1}' +} + +# Format and colorize in Android Studio style +format_log() { + while IFS= read -r line; do + # Format: MM-DD HH:MM:SS.mmm PID TID LEVEL TAG: MESSAGE + # Regex to extract components + if [[ "$line" =~ ^([0-9]{2}-[0-9]{2})\ ([0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+)\ +([0-9]+)\ +([0-9]+)\ ([VDIWEFA])\ ([^:]+):\ (.*)$ ]]; then + date="${BASH_REMATCH[1]}" + time="${BASH_REMATCH[2]}" + pid="${BASH_REMATCH[3]}" + tid="${BASH_REMATCH[4]}" + level="${BASH_REMATCH[5]}" + tag="${BASH_REMATCH[6]}" + msg="${BASH_REMATCH[7]}" + + # Color based on level + color="${COLORS[$level]:-$RESET}" + + # Format: TIME PID-TID/TAG LEVEL: MESSAGE + printf "${DIM}%s${RESET} ${DIM}%5s-%-5s${RESET} ${color}%-20s %s: %s${RESET}\n" \ + "$time" "$pid" "$tid" "$tag" "$level" "$msg" + else + # Unformatted line (continuation or different format) + echo -e "${DIM}$line${RESET}" + fi + done +} + +print_header() { + local pkg=$1 + local pid=$2 + echo "" + echo -e "${BOLD}━━━ $pkg ${DIM}PID: ${pid:-?}${RESET}" + echo "" +} + +cleanup() { + echo "" + echo -e "${DIM}Stopped${RESET}" + pkill -P $$ 2>/dev/null + exit 0 +} + +trap cleanup SIGINT SIGTERM + +# Initial header +clear +echo -e "${BOLD}Logcat${RESET} ${DIM}| Flet/Flutter${RESET}" +echo "" +echo -e "${DIM}Usage: ./flet_log.sh [extra_filter]${RESET}" +echo -e "${DIM}Example: ./flet_log.sh \"OneSignal|Firebase\"${RESET}" +echo "" +echo -e "${DIM}Default filter: flutter, python, Error, Exception, Traceback${RESET}" +[[ -n "$EXTRA_FILTER" ]] && echo -e "${DIM}Extra filter: $EXTRA_FILTER${RESET}" +echo -e "${DIM}Press Ctrl+C to exit${RESET}" + +CURRENT_PKG="" +CURRENT_PID="" + +while true; do + PKG=$(get_package) + + [[ -z "$PKG" ]] && { sleep 1; continue; } + + PID=$(get_pid "$PKG") + + if [[ "$PKG" != "$CURRENT_PKG" || "$PID" != "$CURRENT_PID" ]]; then + pkill -P $$ -f "adb logcat" 2>/dev/null + sleep 0.2 + + CURRENT_PKG="$PKG" + CURRENT_PID="$PID" + + print_header "$PKG" "$PID" + adb logcat -c 2>/dev/null + + if [[ -n "$PID" ]]; then + adb logcat --pid="$PID" -v threadtime 2>/dev/null | \ + grep -E --line-buffered -i "$FILTER" | \ + format_log & + else + adb logcat -v threadtime 2>/dev/null | \ + grep -E --line-buffered -i "$PKG|$FILTER" | \ + format_log & + fi + fi + + sleep 1 +done \ No newline at end of file diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/__init__.py b/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/{{package_name}}/__init__.py b/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/{{package_name}}/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/{{package_name}}/analysis_options.yaml b/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/{{package_name}}/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/{{package_name}}/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/{{package_name}}/lib/src/extension.dart.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/{{package_name}}/lib/src/extension.dart.jinja new file mode 100644 index 0000000..566a90f --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/{{package_name}}/lib/src/extension.dart.jinja @@ -0,0 +1,15 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/widgets.dart'; +import '{{ control_name_snake }}_control.dart'; + +class Extension extends FletExtension { + @override + Widget? createWidget(Key? key, Control control) { + switch (control.type) { + case "{{ control_name }}": + return {{ control_name }}Control(control: control); + default: + return null; + } + } +} diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/{{package_name}}/lib/src/{{control_name_snake}}_control.dart.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/{{package_name}}/lib/src/{{control_name_snake}}_control.dart.jinja new file mode 100644 index 0000000..61f2398 --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/{{package_name}}/lib/src/{{control_name_snake}}_control.dart.jinja @@ -0,0 +1,19 @@ +import 'package:flet/flet.dart'; +import 'package:flutter/material.dart'; + +class {{ control_name }}Control extends StatelessWidget { + final Control control; + const {{ control_name }}Control({super.key, required this.control}); + + @override + Widget build(BuildContext context) { + // TODO: Replace with your actual Flutter widget + Widget myWidget = Container( + color: control.getColor("color", context), + child: const Center( + child: Text("{{ control_name }}"), + ), + ); + return LayoutControl(control: control, child: myWidget); + } +} diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/{{package_name}}/lib/{{package_name}}.dart.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/{{package_name}}/lib/{{package_name}}.dart.jinja new file mode 100644 index 0000000..61c146e --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/{{package_name}}/lib/{{package_name}}.dart.jinja @@ -0,0 +1,3 @@ +library {{ package_name }}; + +export 'src/extension.dart' show Extension; diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/{{package_name}}/pubspec.yaml.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/{{package_name}}/pubspec.yaml.jinja new file mode 100644 index 0000000..0a51b4a --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/src/flutter/{{package_name}}/pubspec.yaml.jinja @@ -0,0 +1,20 @@ +name: {{ package_name }} +description: "{{ description }}" +version: 0.1.0 + +environment: + sdk: ^3.5.0 + flutter: ">=3.29.0" + +dependencies: + flutter: + sdk: flutter + flet: ^0.80.5 + {{ flutter_package }}: {{ flutter_package_version | default("any") }} + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/src/{{package_name}}/__init__.py.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/src/{{package_name}}/__init__.py.jinja new file mode 100644 index 0000000..a096d55 --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/src/{{package_name}}/__init__.py.jinja @@ -0,0 +1,4 @@ +from {{ package_name }}.{{ control_name_snake }} import {{ control_name }} +from {{ package_name }}.types import {{ control_name }}ErrorEvent + +__all__ = ["{{ control_name }}", "{{ control_name }}ErrorEvent"] diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/src/{{package_name}}/console.py.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/src/{{package_name}}/console.py.jinja new file mode 100644 index 0000000..ea4d80f --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/src/{{package_name}}/console.py.jinja @@ -0,0 +1,436 @@ +""" +Debug Console module for {{ package_name }}. + +Provides visual debugging tools for viewing application logs directly +in Flet apps during development. Includes file-based logging with rotation +and a log viewer dialog with color-coded levels and filtering. +""" + +import logging +import os +from datetime import datetime +from enum import Enum +from logging.handlers import RotatingFileHandler +from typing import Optional + +import flet as ft + + +class LogLevel(Enum): + """Log level with associated Flet colors for the DebugConsole display.""" + + DEBUG = ("DEBUG", ft.Colors.GREY_600) + INFO = ("INFO", ft.Colors.BLUE_600) + WARNING = ("WARNING", ft.Colors.AMBER_700) + ERROR = ("ERROR", ft.Colors.RED_600) + CRITICAL = ("CRITICAL", ft.Colors.RED_900) + + @property + def color(self) -> str: + return self.value[1] + + @classmethod + def from_string(cls, level: str) -> "LogLevel": + """Get LogLevel from string.""" + level_map = { + "DEBUG": cls.DEBUG, + "INFO": cls.INFO, + "WARNING": cls.WARNING, + "ERROR": cls.ERROR, + "CRITICAL": cls.CRITICAL, + } + return level_map.get(level.upper(), cls.INFO) + + +def get_log_path() -> str: + """Get the log file path.""" + log_path = os.getenv("FLET_APP_CONSOLE") + if log_path: + return log_path + + import tempfile + + try: + test_path = "debug.log" + with open(test_path, "a"): + pass + return test_path + except (OSError, IOError): + pass + + return os.path.join(tempfile.gettempdir(), "{{ package_name }}_debug.log") + + +def _tail_file(filepath: str, num_lines: int) -> list[str]: + """Efficiently read the last N lines of a file.""" + try: + with open(filepath, "rb") as f: + f.seek(0, 2) + file_size = f.tell() + + if file_size == 0: + return [] + + buffer_size = min(8192, file_size) + lines: list[str] = [] + remaining = file_size + + while remaining > 0 and len(lines) <= num_lines: + read_size = min(buffer_size, remaining) + f.seek(remaining - read_size) + chunk = f.read(read_size) + remaining -= read_size + + text = chunk.decode("utf-8", errors="replace") + chunk_lines = text.splitlines(keepends=True) + + if lines and chunk_lines: + lines[0] = chunk_lines[-1] + lines[0] + chunk_lines = chunk_lines[:-1] + + lines = chunk_lines + lines + + return [line.strip() for line in lines[-num_lines:] if line.strip()] + except Exception: + return [] + + +def _parse_log_line(line: str) -> tuple[str, LogLevel, str]: + """ + Fast parse of log line. Returns (timestamp, level, message). + + Expected format: [timestamp] [LEVEL] ... - message + """ + try: + # Find first bracket pair for timestamp + if line.startswith("["): + end_ts = line.find("]") + if end_ts > 0: + timestamp = line[1:end_ts] + rest = line[end_ts + 1 :].lstrip() + + # Find level in second bracket + if rest.startswith("["): + end_lvl = rest.find("]") + if end_lvl > 0: + level_str = rest[1:end_lvl] + level = LogLevel.from_string(level_str) + + # Find message after " - " + msg_start = rest.find(" - ", end_lvl) + if msg_start > 0: + message = rest[msg_start + 3 :] + else: + message = rest[end_lvl + 1 :].lstrip() + + return timestamp, level, message + except Exception: + pass + + # Fallback: return line as-is + return datetime.now().strftime("%H:%M:%S"), LogLevel.INFO, line + + +def setup_logging( + level: int = logging.INFO, + format_string: str = "[{asctime}] [{levelname}] [{filename}:{funcName}:{lineno}] - {message}", + max_bytes: int = 256 * 1024, # 256 KB max (reduced) + backup_count: int = 1, +) -> logging.Logger: + """Setup file-based logging with automatic rotation for use with DebugConsole. + + Writes logs to the path specified by the ``FLET_APP_CONSOLE`` environment + variable, or to ``debug.log`` in the current directory, or to a temp file + as fallback. + + Args: + level: Logging level (default: ``logging.INFO``). + format_string: Log format string using ``{}``-style formatting. + max_bytes: Maximum log file size in bytes before rotation (default: 256 KB). + backup_count: Number of rotated backup files to keep (default: 1). + + Returns: + Configured logger instance. + + Example: + ```python + import {{ package_name }} as pkg + + logger = pkg.setup_logging() + logger.info("App started") + ``` + """ + log_path = get_log_path() + + file_handler = RotatingFileHandler( + log_path, + mode="a", + maxBytes=max_bytes, + backupCount=backup_count, + encoding="utf-8", + ) + file_handler.setFormatter(logging.Formatter(format_string, style="{")) + + console_handler = logging.StreamHandler() + console_handler.setFormatter(logging.Formatter(format_string, style="{")) + + logger = logging.getLogger() + logger.setLevel(level) + + for handler in logger.handlers[:]: + logger.removeHandler(handler) + + logger.addHandler(file_handler) + logger.addHandler(console_handler) + + return logging.getLogger(__name__) + + +class DebugConsole: + """Lightweight debug console for viewing application logs. + + Provides an ``icon`` (for AppBar) and a ``fab`` (floating action button) that + open a dialog showing application logs with color-coded levels and filtering. + + Reads log entries written by ``setup_logging()``. + + Args: + title: Dialog title (default: ``"Debug Console"``). + max_lines: Maximum number of log lines to display (default: 200). + + Example: + ```python + import {{ package_name }} as pkg + + logger = pkg.setup_logging() + debug = pkg.DebugConsole() + + # Use icon in AppBar + page.appbar = ft.AppBar( + title=ft.Text("My App"), + actions=[debug.icon], + ) + + # Or use FAB + page.floating_action_button = debug.fab + ``` + """ + + MAX_DISPLAY_LINES = 200 + """Maximum lines to display (performance limit).""" + + def __init__( + self, + title: str = "Debug Console", + max_lines: int = 200, + ): + self._title = title + self._max_lines = min(max_lines, self.MAX_DISPLAY_LINES) + self._page: Optional[ft.Page] = None + self._dialog: Optional[ft.AlertDialog] = None + self._log_column: Optional[ft.Column] = None + self._cached_lines: list[str] = [] + self._current_filter: Optional[LogLevel] = None + + # Simple icon without badge + self._icon = ft.IconButton( + icon=ft.Icons.BUG_REPORT_OUTLINED, + tooltip="Debug Console", + on_click=self._show_console, + ) + + # FAB + self._fab = ft.FloatingActionButton( + icon=ft.Icons.BUG_REPORT, + tooltip="Debug Console", + on_click=self._show_console, + bgcolor=ft.Colors.BLUE_GREY_700, + ) + + @property + def icon(self) -> ft.IconButton: + """Get the debug icon for use in AppBar actions.""" + return self._icon + + @property + def fab(self) -> ft.FloatingActionButton: + """Get the FAB for use as floating action button.""" + return self._fab + + def _read_logs(self) -> list[str]: + """Read logs from file (cached).""" + log_path = get_log_path() + if not os.path.exists(log_path): + return [] + return _tail_file(log_path, self._max_lines) + + def _create_log_text(self, line: str) -> ft.Text: + """Create a simple Text widget for a log line.""" + timestamp, level, message = _parse_log_line(line) + + # Format: [HH:MM:SS] [LEVEL] message + short_ts = timestamp.split()[-1] if " " in timestamp else timestamp + display = f"[{short_ts}] [{level.value[0][:4]}] {message}" + + return ft.Text( + display, + size=11, + color=level.color, + selectable=True, + no_wrap=False, + ) + + def _build_log_controls( + self, lines: list[str], filter_level: Optional[LogLevel] = None + ) -> list[ft.Text]: + """Build log text controls from lines.""" + controls = [] + + for line in lines: + if filter_level: + _, level, _ = _parse_log_line(line) + if filter_level == LogLevel.ERROR: + if level not in (LogLevel.ERROR, LogLevel.CRITICAL): + continue + elif level != filter_level: + continue + + controls.append(self._create_log_text(line)) + + return controls + + def _show_console(self, e): + """Show the console dialog.""" + if e.control.page: + self._page = e.control.page + + if not self._page: + return + + # Read and cache logs + self._cached_lines = self._read_logs() + self._current_filter = None + + # Build log controls + log_controls = self._build_log_controls(self._cached_lines) + + # Create scrollable column for logs + self._log_column = ft.Column( + controls=log_controls + if log_controls + else [ft.Text("No logs available", italic=True, color=ft.Colors.GREY_500)], + spacing=2, + scroll=ft.ScrollMode.AUTO, + auto_scroll=True, + expand=True, + ) + + # Create dialog + self._dialog = ft.AlertDialog( + title=ft.Row( + controls=[ + ft.Icon(ft.Icons.BUG_REPORT, color=ft.Colors.BLUE_600, size=20), + ft.Text(self._title, weight=ft.FontWeight.BOLD, size=16), + ft.Container(expand=True), + ft.Text( + f"{len(log_controls)} lines", + size=11, + color=ft.Colors.GREY_600, + ), + ], + spacing=8, + ), + content=ft.Container( + content=ft.Column( + controls=[ + # Filter buttons + ft.Row( + controls=[ + ft.TextButton("All", on_click=lambda _: self._apply_filter(None)), + ft.TextButton( + "Errors", + on_click=lambda _: self._apply_filter(LogLevel.ERROR), + style=ft.ButtonStyle(color=ft.Colors.RED_600), + ), + ft.TextButton( + "Warnings", + on_click=lambda _: self._apply_filter(LogLevel.WARNING), + style=ft.ButtonStyle(color=ft.Colors.AMBER_700), + ), + ], + spacing=0, + ), + ft.Divider(height=1), + # Log content + ft.Container( + content=self._log_column, + expand=True, + padding=8, + border=ft.border.all(1, ft.Colors.GREY_400), + border_radius=4, + ), + ], + expand=True, + spacing=4, + ), + width=500, + height=350, + ), + actions=[ + ft.TextButton("Clear", on_click=self._clear_logs), + ft.TextButton("Refresh", on_click=self._refresh_logs), + ft.TextButton("Close", on_click=self._close_dialog), + ], + actions_alignment=ft.MainAxisAlignment.END, + ) + + # Use Flet 0.80.x dialog API + self._page.show_dialog(self._dialog) + + def _apply_filter(self, level: Optional[LogLevel]): + """Apply filter to cached logs.""" + if not self._log_column or not self._page: + return + + self._current_filter = level + log_controls = self._build_log_controls(self._cached_lines, level) + + if log_controls: + self._log_column.controls = log_controls + else: + filter_name = level.value[0] if level else "matching" + self._log_column.controls = [ + ft.Text(f"No {filter_name} logs found", italic=True, color=ft.Colors.GREY_500) + ] + + self._page.update() + + def _clear_logs(self, e): + """Clear logs from file.""" + try: + log_path = get_log_path() + if os.path.exists(log_path): + with open(log_path, "w", encoding="utf-8") as f: + f.truncate(0) + except Exception: + pass + + self._cached_lines = [] + + if self._log_column: + self._log_column.controls = [ + ft.Text("Logs cleared", italic=True, color=ft.Colors.GREY_500) + ] + + if self._page: + self._page.update() + + def _refresh_logs(self, e): + """Refresh logs from file.""" + self._cached_lines = self._read_logs() + self._apply_filter(self._current_filter) + + def _close_dialog(self, e): + """Close the dialog.""" + if self._page: + self._page.pop_dialog() diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/src/{{package_name}}/types.py.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/src/{{package_name}}/types.py.jinja new file mode 100644 index 0000000..7491fcf --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/src/{{package_name}}/types.py.jinja @@ -0,0 +1,12 @@ +from dataclasses import dataclass +from typing import Optional +import flet as ft + + +@dataclass +class {{ control_name }}ErrorEvent(ft.Event["{{ control_name }}"]): + """Event fired when an error occurs.""" + + method: Optional[str] = None + message: Optional[str] = None + stack_trace: Optional[str] = None diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/src/{{package_name}}/{{control_name_snake}}.py.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/src/{{package_name}}/{{control_name_snake}}.py.jinja new file mode 100644 index 0000000..bcf0462 --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/src/{{package_name}}/{{control_name_snake}}.py.jinja @@ -0,0 +1,16 @@ +from typing import Optional +import flet as ft + +from {{ package_name }}.types import {{ control_name }}ErrorEvent + + +@ft.control("{{ control_name }}") +class {{ control_name }}(ft.LayoutControl): + """{{ description }}""" + + # TODO: Add your visual properties here + # color: Optional[ft.ColorValue] = None + # size: float = 100.0 + + on_error: Optional[ft.EventHandler[{{ control_name }}ErrorEvent]] = None + """Called when an error occurs in the SDK.""" diff --git a/src/flet_pkg/templates/ui_control/{{project_name}}/tests/test_{{package_name}}.py.jinja b/src/flet_pkg/templates/ui_control/{{project_name}}/tests/test_{{package_name}}.py.jinja new file mode 100644 index 0000000..73574f2 --- /dev/null +++ b/src/flet_pkg/templates/ui_control/{{project_name}}/tests/test_{{package_name}}.py.jinja @@ -0,0 +1,6 @@ +from {{ package_name }} import {{ control_name }} + + +def test_import(): + """Test that {{ control_name }} can be imported.""" + assert {{ control_name }} is not None diff --git a/src/flet_pkg/ui/__init__.py b/src/flet_pkg/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/flet_pkg/ui/console.py b/src/flet_pkg/ui/console.py new file mode 100644 index 0000000..90b1fcd --- /dev/null +++ b/src/flet_pkg/ui/console.py @@ -0,0 +1,15 @@ +from rich.console import Console +from rich.theme import Theme + +theme = Theme( + { + "success": "bold green", + "error": "bold red", + "info": "bold cyan", + "highlight": "bold magenta", + "muted": "dim", + "warning": "bold yellow", + } +) + +console = Console(theme=theme) diff --git a/src/flet_pkg/ui/coverage.py b/src/flet_pkg/ui/coverage.py new file mode 100644 index 0000000..7ac7a08 --- /dev/null +++ b/src/flet_pkg/ui/coverage.py @@ -0,0 +1,92 @@ +"""Coverage score display and Rich Table breakdown.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from rich.table import Table + +from flet_pkg.ui.console import console + +if TYPE_CHECKING: + from flet_pkg.core.ai.models import GapReport + + +def _progress_bar(pct: float, width: int = 10) -> str: + """Return a block-char progress bar like ``████████░░``.""" + filled = round(pct / 100 * width) + return "\u2588" * filled + "\u2591" * (width - filled) + + +def _pct_color(pct: float) -> str: + if pct >= 90: + return "green" + if pct >= 70: + return "yellow" + return "red" + + +def print_coverage_score( + pct: float, + total_generated: int, + total_dart_api: int, + ai_pct: float | None = None, +) -> None: + """Always-visible one-line coverage score, with optional AI comparison.""" + if total_dart_api == 0: + console.print(" [info]Coverage: N/A (no Dart API found)[/info]") + return + + color = _pct_color(pct) + + if ai_pct is not None and ai_pct > pct: + ai_color = _pct_color(ai_pct) + delta = ai_pct - pct + console.print( + f" [{color}]Coverage: {pct:.1f}%[/{color}] " + f"[dim]->[/dim] [{ai_color}]{ai_pct:.1f}%[/{ai_color}] " + f"[dim](+{delta:.1f}% with AI)[/dim] " + f"({total_generated}/{total_dart_api} features mapped)" + ) + else: + console.print( + f" [{color}]Coverage: {pct:.1f}%[/{color}] " + f"({total_generated}/{total_dart_api} features mapped)" + ) + + +def print_coverage_table(report: GapReport) -> None: + """Verbose Rich Table showing per-category breakdown.""" + if not report.category_counts: + return + + table = Table( + title="Coverage Breakdown", + show_header=True, + header_style="bold", + title_style="bold", + padding=(0, 1), + ) + table.add_column("Category", style="cyan", min_width=12) + table.add_column("Dart API", justify="right", min_width=8) + table.add_column("Mapped", justify="right", min_width=7) + table.add_column("Coverage", min_width=16) + + for cat, (dart_api, mapped) in report.category_counts.items(): + pct = (mapped / dart_api * 100) if dart_api > 0 else 100.0 + bar = _progress_bar(pct) + table.add_row(cat, str(dart_api), str(mapped), f"{bar} {pct:.0f}%") + + # Total row + table.add_section() + total_pct = report.coverage_pct + bar = _progress_bar(total_pct) + table.add_row( + "[bold]Total[/bold]", + f"[bold]{report.total_dart_api}[/bold]", + f"[bold]{report.total_generated}[/bold]", + f"[bold]{bar} {total_pct:.0f}%[/bold]", + ) + + console.print() + console.print(table) diff --git a/src/flet_pkg/ui/panels.py b/src/flet_pkg/ui/panels.py new file mode 100644 index 0000000..6940212 --- /dev/null +++ b/src/flet_pkg/ui/panels.py @@ -0,0 +1,22 @@ +from rich.panel import Panel + +from flet_pkg.ui.console import console + + +def header_panel(title: str, subtitle: str = "") -> None: + content = f"[info]{subtitle}[/info]" if subtitle else "" + console.print( + Panel(content, title=f"[highlight]{title}[/highlight]", border_style="bright_blue") + ) + + +def success_panel(title: str, content: str) -> None: + console.print(Panel(content, title=f"[success]{title}[/success]", border_style="green")) + + +def error_panel(title: str, content: str) -> None: + console.print(Panel(content, title=f"[error]{title}[/error]", border_style="red")) + + +def info_panel(title: str, content: str) -> None: + console.print(Panel(content, title=f"[info]{title}[/info]", border_style="cyan")) diff --git a/src/flet_pkg/ui/tree.py b/src/flet_pkg/ui/tree.py new file mode 100644 index 0000000..8828b2a --- /dev/null +++ b/src/flet_pkg/ui/tree.py @@ -0,0 +1,30 @@ +from pathlib import Path + +from rich.panel import Panel +from rich.tree import Tree + +from flet_pkg.ui.console import console + + +def build_tree(root_path: Path, label: str | None = None) -> Tree: + tree = Tree(f"[highlight]{label or root_path.name}/[/highlight]") + _walk(root_path, tree) + return tree + + +def _walk(path: Path, tree: Tree) -> None: + entries = sorted(path.iterdir(), key=lambda p: (p.is_file(), p.name)) + for entry in entries: + if entry.name.startswith("."): + continue + if entry.is_dir(): + branch = tree.add(f"[info]{entry.name}/[/info]") + _walk(entry, branch) + else: + tree.add(f"[muted]{entry.name}[/muted]") + + +def print_tree(root_path: Path, title: str = "Generated Project") -> None: + tree = build_tree(root_path) + console.print() + console.print(Panel(tree, title=f"[success]{title}[/success]", border_style="green")) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..8bb9b2e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +from pathlib import Path + +import pytest + + +@pytest.fixture +def tmp_output(tmp_path: Path) -> Path: + return tmp_path / "output" diff --git a/tests/fixtures/onesignal_flutter/lib/onesignal_flutter.dart b/tests/fixtures/onesignal_flutter/lib/onesignal_flutter.dart new file mode 100644 index 0000000..d13f1ea --- /dev/null +++ b/tests/fixtures/onesignal_flutter/lib/onesignal_flutter.dart @@ -0,0 +1,7 @@ +library onesignal_flutter; + +export 'src/onesignal.dart'; +export 'src/user.dart'; +export 'src/notifications.dart'; +export 'src/debug.dart'; +export 'src/types.dart'; diff --git a/tests/fixtures/onesignal_flutter/lib/src/debug.dart b/tests/fixtures/onesignal_flutter/lib/src/debug.dart new file mode 100644 index 0000000..18b4bae --- /dev/null +++ b/tests/fixtures/onesignal_flutter/lib/src/debug.dart @@ -0,0 +1,21 @@ +/// Debug log levels. +enum OSLogLevel { + none, + fatal, + error, + warn, + info, + debug, + verbose, +} + +/// OneSignal Debug namespace. +class OneSignalDebug { + OneSignalDebug._(); + + /// Set the log level. + void setLogLevel(OSLogLevel level) {} + + /// Set the alert level. + void setAlertLevel(OSLogLevel level) {} +} diff --git a/tests/fixtures/onesignal_flutter/lib/src/notifications.dart b/tests/fixtures/onesignal_flutter/lib/src/notifications.dart new file mode 100644 index 0000000..f7e222b --- /dev/null +++ b/tests/fixtures/onesignal_flutter/lib/src/notifications.dart @@ -0,0 +1,43 @@ +/// OneSignal Notifications namespace. +/// +/// Manages push notification permissions and display. +class OneSignalNotifications { + OneSignalNotifications._(); + + /// Request notification permission. + Future requestPermission(bool fallbackToSettings) async { + return false; + } + + /// Check if can request permission. + Future canRequest() async { + return false; + } + + /// Get current permission status. + bool get permission => false; + + /// Clear all notifications. + Future clearAll() async {} + + /// Remove a notification. + Future removeNotification(int notificationId) async {} + + /// Remove grouped notifications. + Future removeGroupedNotifications(String group) async {} + + /// Prevent default display. + void preventDefault(String notificationId) {} + + /// Display a notification. + void displayNotification(String notificationId) {} + + /// Add click listener. + void addClickListener(Function(dynamic event) callback) {} + + /// Add foreground will display listener. + void addForegroundWillDisplayListener(Function(dynamic event) callback) {} + + /// Add permission observer. + void addPermissionObserver(Function(bool permission) callback) {} +} diff --git a/tests/fixtures/onesignal_flutter/lib/src/onesignal.dart b/tests/fixtures/onesignal_flutter/lib/src/onesignal.dart new file mode 100644 index 0000000..568485a --- /dev/null +++ b/tests/fixtures/onesignal_flutter/lib/src/onesignal.dart @@ -0,0 +1,23 @@ +/// OneSignal main class. +/// +/// The main entry point for the OneSignal SDK. +class OneSignal { + /// Initialize OneSignal with the given app ID. + static void initialize(String appId) {} + + /// Login with an external user ID. + static Future login(String externalId) async {} + + /// Logout the current user. + static Future logout() async {} + + /// Set consent given status. + static Future consentGiven(bool given) async {} + + /// Set consent required. + static void consentRequired(bool required) {} + + static OneSignalDebug get Debug => OneSignalDebug._(); + static OneSignalUser get User => OneSignalUser._(); + static OneSignalNotifications get Notifications => OneSignalNotifications._(); +} diff --git a/tests/fixtures/onesignal_flutter/lib/src/types.dart b/tests/fixtures/onesignal_flutter/lib/src/types.dart new file mode 100644 index 0000000..59699b2 --- /dev/null +++ b/tests/fixtures/onesignal_flutter/lib/src/types.dart @@ -0,0 +1,6 @@ +/// Permission status enum. +enum PermissionStatus { + granted, + denied, + notDetermined, +} diff --git a/tests/fixtures/onesignal_flutter/lib/src/user.dart b/tests/fixtures/onesignal_flutter/lib/src/user.dart new file mode 100644 index 0000000..ecf1ad2 --- /dev/null +++ b/tests/fixtures/onesignal_flutter/lib/src/user.dart @@ -0,0 +1,63 @@ +/// OneSignal User namespace. +/// +/// Manages user identity, tags, and subscriptions. +class OneSignalUser { + OneSignalUser._(); + + /// Get the OneSignal ID. + Future getOnesignalId() async { + return null; + } + + /// Get the external ID. + Future getExternalId() async { + return null; + } + + /// Add a tag with key and value. + Future addTagWithKey(String key, String value) async {} + + /// Add multiple tags. + Future addTags(Map tags) async {} + + /// Remove a tag by key. + Future removeTag(String key) async {} + + /// Remove multiple tags. + Future removeTags(List keys) async {} + + /// Get all tags. + Future> getTags() async { + return {}; + } + + /// Add a single alias. + Future addAlias(String label, String id) async {} + + /// Add multiple aliases. + Future addAliases(Map aliases) async {} + + /// Remove a single alias. + Future removeAlias(String label) async {} + + /// Remove multiple aliases. + Future removeAliases(List labels) async {} + + /// Add an email. + Future addEmail(String email) async {} + + /// Remove an email. + Future removeEmail(String email) async {} + + /// Add SMS number. + Future addSms(String phone) async {} + + /// Remove SMS number. + Future removeSms(String phone) async {} + + /// Set user language. + Future setLanguage(String language) async {} + + /// Add user change observer. + void addObserver(Function(dynamic state) callback) {} +} diff --git a/tests/test_ai_refiner.py b/tests/test_ai_refiner.py new file mode 100644 index 0000000..d20ef20 --- /dev/null +++ b/tests/test_ai_refiner.py @@ -0,0 +1,546 @@ +"""Tests for the AI refinement pipeline. + +All tests use mocked LLM responses — no API key required. +""" + +from __future__ import annotations + +import os +from pathlib import Path +from unittest.mock import patch + +import pytest + +from flet_pkg.core.ai.config import AIConfig +from flet_pkg.core.ai.gap_analyzer import GapAnalyzer +from flet_pkg.core.ai.models import ( + ArchitectPlan, + FileEdit, + GapItem, + GapKind, + GapReport, + ImprovementSuggestion, + RefinementResult, +) +from flet_pkg.core.models import ( + DartClass, + DartEnum, + DartMethod, + DartPackageAPI, + DartParam, + EnumPlan, + EventPlan, + GenerationPlan, + MethodPlan, + PropertyPlan, +) + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture() +def service_api(): + """A simple Dart API for a service package.""" + return DartPackageAPI( + classes=[ + DartClass( + name="OneSignal", + methods=[ + DartMethod(name="initialize", return_type="Future", is_async=True), + DartMethod(name="getDeviceState", return_type="Future"), + DartMethod(name="setLogLevel", return_type="void"), + DartMethod( + name="onNotificationReceived", + return_type="Stream", + is_getter=True, + ), + # Internal method — should be excluded + DartMethod(name="toString", return_type="String"), + ], + ), + ], + enums=[ + DartEnum(name="OSLogLevel", values=[("verbose", ""), ("debug", "")]), + ], + ) + + +@pytest.fixture() +def service_plan(): + """A partially complete generation plan for a service package.""" + return GenerationPlan( + control_name="OneSignal", + package_name="flet_onesignal", + flutter_package="onesignal_flutter", + main_methods=[ + MethodPlan(python_name="initialize"), + MethodPlan(python_name="get_device_state"), + # set_log_level is missing → gap + ], + events=[ + EventPlan( + python_attr_name="on_notification_received", + event_class_name="NotificationEvent", + ), + ], + enums=[ + EnumPlan(python_name="OSLogLevel", values=[("VERBOSE", "verbose", "")]), + ], + ) + + +@pytest.fixture() +def ui_control_api(): + """A simple Dart API for a UI control package.""" + return DartPackageAPI( + classes=[ + DartClass( + name="Shimmer", + constructor_params=[ + DartParam(name="key", dart_type="Key?"), + DartParam(name="child", dart_type="Widget"), + DartParam(name="baseColor", dart_type="Color"), + DartParam(name="highlightColor", dart_type="Color"), + DartParam(name="enabled", dart_type="bool"), + DartParam(name="direction", dart_type="ShimmerDirection"), + DartParam(name="gradient", dart_type="LinearGradient?"), + ], + ), + ], + enums=[ + DartEnum(name="ShimmerDirection", values=[("ltr", ""), ("rtl", "")]), + ], + ) + + +@pytest.fixture() +def ui_control_plan(): + """A partially complete generation plan for a UI control.""" + return GenerationPlan( + control_name="Shimmer", + package_name="flet_shimmer", + flutter_package="shimmer", + dart_main_class="Shimmer", + base_class="ft.LayoutControl", + properties=[ + PropertyPlan(python_name="base_color", python_type="ft.Color"), + PropertyPlan(python_name="highlight_color", python_type="ft.Color"), + PropertyPlan(python_name="enabled", python_type="bool"), + # direction is missing → gap + # gradient is non-serializable → infeasible gap + ], + enums=[ + EnumPlan(python_name="ShimmerDirection", values=[("LTR", "ltr", "")]), + ], + ) + + +# --------------------------------------------------------------------------- +# TestGapAnalyzer +# --------------------------------------------------------------------------- + + +class TestGapAnalyzer: + def test_service_gap_detection(self, service_api, service_plan): + analyzer = GapAnalyzer() + report = analyzer.analyze(service_api, service_plan, "service") + + assert report.flutter_package == "onesignal_flutter" + assert report.extension_type == "service" + assert report.coverage_pct > 0 + + # set_log_level should be a gap + method_gaps = [g for g in report.gaps if g.kind == GapKind.MISSING_METHOD] + assert len(method_gaps) == 1 + assert method_gaps[0].dart_name == "setLogLevel" + + # No enum gaps — OSLogLevel was generated + enum_gaps = [g for g in report.gaps if g.kind == GapKind.MISSING_ENUM] + assert len(enum_gaps) == 0 + + def test_service_excludes_internal_methods(self, service_api, service_plan): + analyzer = GapAnalyzer() + report = analyzer.analyze(service_api, service_plan, "service") + + # toString should not appear as a gap + dart_names = {g.dart_name for g in report.gaps} + assert "toString" not in dart_names + + def test_service_stream_event_detected(self, service_api, service_plan): + analyzer = GapAnalyzer() + report = analyzer.analyze(service_api, service_plan, "service") + + # onNotificationReceived stream was mapped → no event gap + event_gaps = [g for g in report.gaps if g.kind == GapKind.MISSING_EVENT] + assert len(event_gaps) == 0 + + def test_ui_control_gap_detection(self, ui_control_api, ui_control_plan): + analyzer = GapAnalyzer() + report = analyzer.analyze(ui_control_api, ui_control_plan, "ui_control") + + assert report.flutter_package == "shimmer" + assert report.extension_type == "ui_control" + + # direction should be a feasible gap + prop_gaps = [g for g in report.gaps if g.kind == GapKind.MISSING_PROPERTY] + feasible = [g for g in prop_gaps if g.feasible] + infeasible = [g for g in prop_gaps if not g.feasible] + + assert any(g.dart_name == "direction" for g in feasible) + # gradient is non-serializable → infeasible + assert any(g.dart_name == "gradient" for g in infeasible) + + def test_ui_control_excludes_framework_params(self, ui_control_api, ui_control_plan): + analyzer = GapAnalyzer() + report = analyzer.analyze(ui_control_api, ui_control_plan, "ui_control") + + dart_names = {g.dart_name for g in report.gaps} + assert "key" not in dart_names + assert "child" not in dart_names + + def test_feasible_gaps_count(self, ui_control_api, ui_control_plan): + analyzer = GapAnalyzer() + report = analyzer.analyze(ui_control_api, ui_control_plan, "ui_control") + + feasible = report.feasible_gaps + total = len(report.gaps) + assert feasible <= total + assert feasible >= 0 + + def test_empty_api_produces_empty_report(self): + analyzer = GapAnalyzer() + api = DartPackageAPI() + plan = GenerationPlan( + control_name="Empty", + package_name="flet_empty", + flutter_package="empty", + ) + report = analyzer.analyze(api, plan, "service") + + assert report.total_dart_api == 0 + assert len(report.gaps) == 0 + assert report.coverage_pct == 0.0 + + def test_gap_report_summary(self, service_api, service_plan): + analyzer = GapAnalyzer() + report = analyzer.analyze(service_api, service_plan, "service") + + summary = report.summary() + assert "onesignal_flutter" in summary + assert "service" in summary + + +# --------------------------------------------------------------------------- +# TestAIConfig +# --------------------------------------------------------------------------- + + +class TestAIConfig: + def test_default_config(self): + config = AIConfig() + assert config.provider == "ollama" + assert config.temperature == 0.1 + + def test_load_default_is_ollama(self): + config = AIConfig.load() + assert config.provider == "ollama" + assert config.model == "qwen2.5-coder:14b" + assert config.is_available() + + def test_load_anthropic(self): + with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}, clear=False): + config = AIConfig.load(provider="anthropic") + assert config.provider == "anthropic" + assert config.model == "claude-sonnet-4-6" + assert config.api_key == "test-key" + assert config.is_available() + + def test_load_openai(self): + with patch.dict(os.environ, {"OPENAI_API_KEY": "sk-test"}, clear=False): + config = AIConfig.load(provider="openai") + assert config.provider == "openai" + assert config.model == "gpt-4.1-mini" + assert config.api_key == "sk-test" + + def test_load_ollama_no_key_needed(self): + config = AIConfig.load(provider="ollama") + assert config.provider == "ollama" + assert config.model == "qwen2.5-coder:14b" + assert config.is_available() + + def test_load_with_model_override(self): + config = AIConfig.load(provider="anthropic", model="claude-opus-4-20250514") + assert config.model == "claude-opus-4-20250514" + + def test_not_available_without_key(self): + with patch.dict(os.environ, {}, clear=True): + config = AIConfig.load(provider="anthropic") + assert not config.is_available() + + def test_load_unknown_provider_defaults_to_ollama(self): + config = AIConfig.load(provider="unknown_provider") + assert config.provider == "ollama" + + +# --------------------------------------------------------------------------- +# TestRefinementModels +# --------------------------------------------------------------------------- + + +class TestRefinementModels: + def test_gap_item_creation(self): + gap = GapItem( + kind=GapKind.MISSING_METHOD, + dart_name="getStatus", + dart_type="Future", + dart_class="MyService", + reason="Method not mapped", + ) + assert gap.kind == GapKind.MISSING_METHOD + assert gap.feasible is True + + def test_gap_report_feasible_count(self): + report = GapReport( + flutter_package="test", + extension_type="service", + gaps=[ + GapItem(kind=GapKind.MISSING_METHOD, dart_name="a", feasible=True), + GapItem(kind=GapKind.MISSING_METHOD, dart_name="b", feasible=False), + GapItem(kind=GapKind.MISSING_ENUM, dart_name="c", feasible=True), + ], + ) + assert report.feasible_gaps == 2 + + def test_architect_plan(self): + plan = ArchitectPlan( + analysis="Good coverage, minor gaps", + suggestions=[ + ImprovementSuggestion( + target_file="service.py", + description="Add get_status method", + gap_refs=[0], + ), + ], + ) + assert len(plan.suggestions) == 1 + assert plan.suggestions[0].priority == 1 + + def test_file_edit(self): + edit = FileEdit( + filename="service.py", + search="# TODO", + replace="async def get_status(self): ...", + rationale="Add missing method", + ) + assert edit.filename == "service.py" + + def test_refinement_result_no_gaps(self): + result = RefinementResult( + gap_report=GapReport( + flutter_package="test", + extension_type="service", + ), + validation_passed=True, + overall_assessment="All good", + ) + assert result.edits_applied == 0 + assert result.validation_passed + + +# --------------------------------------------------------------------------- +# TestApplyEdits +# --------------------------------------------------------------------------- + + +class TestApplyEdits: + def test_apply_single_edit(self): + from flet_pkg.core.ai.agent import apply_edits + + files = {"service.py": "class MyService:\n # TODO: add methods\n pass\n"} + edits = [ + FileEdit( + filename="service.py", + search=" # TODO: add methods\n pass", + replace=( + " async def get_status(self) -> str:\n" + ' return await self.invoke_method("get_status")' + ), + ), + ] + + modified, applied, failed = apply_edits(edits, files) + assert applied == 1 + assert failed == 0 + assert "get_status" in modified["service.py"] + + def test_edit_missing_file(self): + from flet_pkg.core.ai.agent import apply_edits + + edits = [FileEdit(filename="nonexistent.py", search="x", replace="y")] + _, applied, failed = apply_edits(edits, {}) + assert applied == 0 + assert failed == 1 + + def test_edit_search_not_found(self): + from flet_pkg.core.ai.agent import apply_edits + + files = {"service.py": "class MyService:\n pass\n"} + edits = [ + FileEdit(filename="service.py", search="not_in_file", replace="replacement"), + ] + _, applied, failed = apply_edits(edits, files) + assert applied == 0 + assert failed == 1 + + def test_multiple_edits(self): + from flet_pkg.core.ai.agent import apply_edits + + files = { + "a.py": "# placeholder a", + "b.py": "# placeholder b", + } + edits = [ + FileEdit(filename="a.py", search="# placeholder a", replace="# done a"), + FileEdit(filename="b.py", search="# placeholder b", replace="# done b"), + ] + modified, applied, failed = apply_edits(edits, files) + assert applied == 2 + assert failed == 0 + assert modified["a.py"] == "# done a" + assert modified["b.py"] == "# done b" + + def test_edits_do_not_mutate_original(self): + from flet_pkg.core.ai.agent import apply_edits + + files = {"a.py": "original"} + edits = [FileEdit(filename="a.py", search="original", replace="modified")] + modified, _, _ = apply_edits(edits, files) + assert files["a.py"] == "original" + assert modified["a.py"] == "modified" + + +# --------------------------------------------------------------------------- +# TestValidation +# --------------------------------------------------------------------------- + + +class TestValidation: + def test_valid_python(self): + from flet_pkg.core.ai.agent import validate_python_syntax + + files = {"good.py": "x = 1\ny = x + 2\n"} + errors = validate_python_syntax(files) + assert errors == [] + + def test_invalid_python(self): + from flet_pkg.core.ai.agent import validate_python_syntax + + files = {"bad.py": "def foo(\n"} + errors = validate_python_syntax(files) + assert len(errors) == 1 + assert "bad.py" in errors[0] + + def test_ignores_dart_files(self): + from flet_pkg.core.ai.agent import validate_python_syntax + + files = {"service.dart": "this is not valid python {{{"} + errors = validate_python_syntax(files) + assert errors == [] + + def test_mixed_valid_and_invalid(self): + from flet_pkg.core.ai.agent import validate_python_syntax + + files = { + "good.py": "x = 1", + "bad.py": "def (", + } + errors = validate_python_syntax(files) + assert len(errors) == 1 + + +# --------------------------------------------------------------------------- +# TestPipelineIntegration +# --------------------------------------------------------------------------- + + +class TestPipelineIntegration: + def test_pipeline_without_ai_flag(self, tmp_path): + """Pipeline runs normally when ai_refine=False.""" + from flet_pkg.core.pipeline import GenerationPipeline + + # Just verify the parameter is accepted without error + pipeline = GenerationPipeline() + assert hasattr(pipeline.run, "__call__") + + def test_pipeline_ai_import_graceful(self, tmp_path): + """Pipeline handles missing pydantic-ai gracefully.""" + from flet_pkg.core.pipeline import GenerationPipeline + + pipeline = GenerationPipeline() + + # Create minimal project structure + project_dir = tmp_path / "test_project" + project_dir.mkdir() + (project_dir / "src" / "flet_test").mkdir(parents=True) + (project_dir / "src" / "flutter" / "flet_test" / "lib" / "src").mkdir(parents=True) + + # Mock the download to use fixture + fixture_path = Path(__file__).parent / "fixtures" / "onesignal_flutter" + if not fixture_path.exists(): + pytest.skip("onesignal_flutter fixture not available") + + with patch.object(pipeline.downloader, "download", return_value=fixture_path): + result = pipeline.run( + flutter_package="onesignal_flutter", + control_name="OneSignal", + extension_type="service", + project_dir=project_dir, + package_name="flet_test", + ai_refine=True, # Will gracefully skip if pydantic-ai not installed + ai_provider="anthropic", + ) + # Should complete without error (AI step is skipped gracefully) + assert result is not None + + +# --------------------------------------------------------------------------- +# TestGapAnalyzerWithFixture +# --------------------------------------------------------------------------- + + +class TestGapAnalyzerWithFixture: + """Test gap analyzer against actual fixture data.""" + + def test_onesignal_fixture(self): + """Test gap analysis with the onesignal_flutter fixture.""" + fixture_path = Path(__file__).parent / "fixtures" / "onesignal_flutter" + if not fixture_path.exists(): + pytest.skip("onesignal_flutter fixture not available") + + from flet_pkg.core.analyzer import PackageAnalyzer + from flet_pkg.core.parser import parse_dart_package_api + + api = parse_dart_package_api(fixture_path) + analyzer = PackageAnalyzer() + plan = analyzer.analyze( + api, + control_name="OneSignal", + extension_type="service", + flutter_package="onesignal_flutter", + package_name="flet_onesignal", + ) + + gap_analyzer = GapAnalyzer() + report = gap_analyzer.analyze(api, plan, "service") + + # Should produce a meaningful report + assert report.total_dart_api > 0 + assert report.coverage_pct > 0 + assert isinstance(report.gaps, list) + + # Coverage should roughly match what test_service_coverage reports + assert report.coverage_pct >= 70.0, ( + f"OneSignal gap analysis coverage {report.coverage_pct:.1f}% < 70%" + ) diff --git a/tests/test_analyzer.py b/tests/test_analyzer.py new file mode 100644 index 0000000..63f07ec --- /dev/null +++ b/tests/test_analyzer.py @@ -0,0 +1,1046 @@ +import pytest + +from flet_pkg.core.analyzer import PackageAnalyzer +from flet_pkg.core.models import ( + DartClass, + DartEnum, + DartMethod, + DartPackageAPI, + DartParam, +) + + +@pytest.fixture +def analyzer(): + return PackageAnalyzer() + + +@pytest.fixture +def sample_api(): + """A realistic DartPackageAPI mirroring a OneSignal-like structure.""" + return DartPackageAPI( + classes=[ + DartClass( + name="OneSignal", + methods=[ + DartMethod( + name="login", + return_type="Future", + params=[DartParam(name="externalId", dart_type="String", required=True)], + is_async=True, + ), + DartMethod(name="logout", return_type="Future", is_async=True), + ], + source_file="src/onesignal.dart", + ), + DartClass( + name="OneSignalUser", + methods=[ + DartMethod( + name="getOnesignalId", + return_type="Future", + is_async=True, + ), + DartMethod( + name="addTagWithKey", + return_type="Future", + params=[ + DartParam(name="key", dart_type="String", required=True), + DartParam(name="value", dart_type="String", required=True), + ], + is_async=True, + ), + DartMethod( + name="removeTag", + return_type="Future", + params=[DartParam(name="key", dart_type="String", required=True)], + is_async=True, + ), + ], + source_file="src/user.dart", + ), + DartClass( + name="OneSignalNotifications", + methods=[ + DartMethod( + name="requestPermission", + return_type="Future", + params=[ + DartParam(name="fallbackToSettings", dart_type="bool", required=True) + ], + is_async=True, + ), + DartMethod(name="clearAll", return_type="Future", is_async=True), + DartMethod( + name="addClickListener", + return_type="void", + params=[DartParam(name="callback", dart_type="Function")], + ), + ], + source_file="src/notifications.dart", + ), + ], + enums=[ + DartEnum( + name="OSLogLevel", + values=[ + ("none", ""), + ("fatal", ""), + ("error", ""), + ("warn", ""), + ("info", ""), + ("debug", ""), + ("verbose", ""), + ], + ), + ], + ) + + +class TestNamespaceDetection: + def test_prefix_detection(self, analyzer, sample_api): + plan = analyzer.analyze( + sample_api, "OneSignal", "service", "onesignal_flutter", "flet_onesignal" + ) + + ns_names = [s.module_name for s in plan.sub_modules] + assert "user" in ns_names + assert "notifications" in ns_names + + def test_main_class_not_namespace(self, analyzer, sample_api): + plan = analyzer.analyze( + sample_api, "OneSignal", "service", "onesignal_flutter", "flet_onesignal" + ) + + ns_names = [s.module_name for s in plan.sub_modules] + # "OneSignal" itself should NOT become a namespace + assert "onesignal" not in [n.lower() for n in ns_names] + + def test_main_methods_from_main_class(self, analyzer, sample_api): + plan = analyzer.analyze( + sample_api, "OneSignal", "service", "onesignal_flutter", "flet_onesignal" + ) + + main_names = [m.python_name for m in plan.main_methods] + assert "login" in main_names + assert "logout" in main_names + + +class TestEventDetection: + def test_detects_click_listener(self, analyzer, sample_api): + plan = analyzer.analyze( + sample_api, "OneSignal", "service", "onesignal_flutter", "flet_onesignal" + ) + + event_names = [e.python_attr_name for e in plan.events] + # Event from OneSignalNotifications gets namespace prefix "notification" + assert "on_notification_click" in event_names + + def test_event_excludes_from_methods(self, analyzer, sample_api): + plan = analyzer.analyze( + sample_api, "OneSignal", "service", "onesignal_flutter", "flet_onesignal" + ) + + # Listener methods should NOT appear as regular methods in sub-modules + for sub in plan.sub_modules: + method_names = [m.python_name for m in sub.methods] + assert "add_click_listener" not in method_names + + +class TestSubModuleMethods: + def test_user_methods(self, analyzer, sample_api): + plan = analyzer.analyze( + sample_api, "OneSignal", "service", "onesignal_flutter", "flet_onesignal" + ) + + user_sub = next(s for s in plan.sub_modules if s.module_name == "user") + method_names = [m.python_name for m in user_sub.methods] + assert "get_onesignal_id" in method_names + assert "add_tag_with_key" in method_names + assert "remove_tag" in method_names + + def test_dart_prefix(self, analyzer, sample_api): + plan = analyzer.analyze( + sample_api, "OneSignal", "service", "onesignal_flutter", "flet_onesignal" + ) + + user_sub = next(s for s in plan.sub_modules if s.module_name == "user") + login_method = next(m for m in user_sub.methods if m.python_name == "get_onesignal_id") + assert login_method.dart_method_name == "user_get_onesignal_id" + + +class TestEnumDetection: + def test_enums_extracted(self, analyzer, sample_api): + plan = analyzer.analyze( + sample_api, "OneSignal", "service", "onesignal_flutter", "flet_onesignal" + ) + + assert len(plan.enums) == 1 + assert plan.enums[0].python_name == "OSLogLevel" + assert len(plan.enums[0].values) == 7 + + +class TestBaseClass: + def test_service_type(self, analyzer, sample_api): + plan = analyzer.analyze( + sample_api, "OneSignal", "service", "onesignal_flutter", "flet_onesignal" + ) + assert plan.base_class == "ft.Service" + + def test_ui_control_type(self, analyzer, sample_api): + plan = analyzer.analyze( + sample_api, "OneSignal", "ui_control", "onesignal_flutter", "flet_onesignal" + ) + assert plan.base_class == "ft.LayoutControl" + + +class TestWidgetSelection: + """Tests for _select_main_widget filtering.""" + + def test_filters_private_classes(self, analyzer): + """Private Dart classes (starting with _) should be filtered.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="_InternalRenderer", + constructor_params=[DartParam(name="size", dart_type="double")], + ), + DartClass( + name="Rive", + constructor_params=[ + DartParam(name="fit", dart_type="BoxFit"), + DartParam(name="alignment", dart_type="Alignment"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "Rive", "ui_control", "rive", "flet_rive") + assert plan.dart_main_class == "Rive" + + def test_filters_internal_suffix_classes(self, analyzer): + """Classes with internal suffixes (SharedTexture, Renderer, etc.) should be filtered.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="RiveSharedTexture", + constructor_params=[ + DartParam(name="x", dart_type="int"), + DartParam(name="y", dart_type="int"), + DartParam(name="w", dart_type="int"), + ], + ), + DartClass( + name="RiveRenderer", + constructor_params=[DartParam(name="path", dart_type="String")], + ), + DartClass( + name="RiveAnimation", + constructor_params=[ + DartParam(name="fit", dart_type="BoxFit"), + DartParam(name="alignment", dart_type="Alignment"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "Rive", "ui_control", "rive", "flet_rive") + # Should pick RiveAnimation, not RiveSharedTexture (most params but internal) + assert plan.dart_main_class == "RiveAnimation" + + def test_fallback_when_all_filtered(self, analyzer): + """If all classes are internal, fall back to unfiltered selection.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="AlphaRenderer", + constructor_params=[DartParam(name="a", dart_type="int")], + ), + DartClass( + name="BetaController", + constructor_params=[ + DartParam(name="b", dart_type="int"), + DartParam(name="c", dart_type="int"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "Gamma", "ui_control", "gamma_pkg", "flet_gamma") + # Should fallback to BetaController (most params in unfiltered list) + assert plan.dart_main_class == "BetaController" + + +class TestFletTypeMapping: + """Tests for Flet-aware type mapping in ui_control analysis.""" + + def test_alignment_becomes_ft_alignment(self, analyzer): + api = DartPackageAPI( + classes=[ + DartClass( + name="MyWidget", + constructor_params=[ + DartParam(name="alignment", dart_type="Alignment"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "MyWidget", "ui_control", "my_widget", "flet_my_widget") + prop = next(p for p in plan.properties if p.python_name == "alignment") + assert prop.python_type == "ft.Alignment | None" + assert "getAlignment" in prop.dart_getter + + def test_box_fit_becomes_ft_box_fit(self, analyzer): + api = DartPackageAPI( + classes=[ + DartClass( + name="MyWidget", + constructor_params=[ + DartParam(name="fit", dart_type="BoxFit"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "MyWidget", "ui_control", "my_widget", "flet_my_widget") + prop = next(p for p in plan.properties if p.python_name == "fit") + assert prop.python_type == "ft.BoxFit | None" + assert "getBoxFit" in prop.dart_getter + + def test_key_is_skipped(self, analyzer): + api = DartPackageAPI( + classes=[ + DartClass( + name="MyWidget", + constructor_params=[ + DartParam(name="key", dart_type="Key?"), + DartParam(name="name", dart_type="String"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "MyWidget", "ui_control", "my_widget", "flet_my_widget") + prop_names = [p.python_name for p in plan.properties] + assert "key" not in prop_names + assert "name" in prop_names + + def test_list_gets_default_factory(self, analyzer): + api = DartPackageAPI( + classes=[ + DartClass( + name="MyWidget", + constructor_params=[ + DartParam(name="items", dart_type="List"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "MyWidget", "ui_control", "my_widget", "flet_my_widget") + prop = next(p for p in plan.properties if p.python_name == "items") + assert prop.python_type == "list[str]" + assert prop.default_value == "field(default_factory=list)" + + def test_service_type_uses_standard_mapping(self, analyzer): + """Service extensions should still use standard map_dart_type, not Flet types.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="MyService", + constructor_params=[ + DartParam(name="alignment", dart_type="Alignment"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "MyService", "service", "my_service", "flet_my_service") + # In service mode, Alignment should map to "str" (standard mapping) + if plan.properties: + prop = next((p for p in plan.properties if p.python_name == "alignment"), None) + if prop: + assert prop.python_type == "str | None" + + +class TestControllerTypeFiltering: + """Tests that Controller/Painter types are filtered from widget properties.""" + + def test_controller_type_filtered(self, analyzer): + """AnimationController params should not become properties.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="RiveWidget", + constructor_params=[ + DartParam(name="fit", dart_type="BoxFit"), + DartParam(name="controller", dart_type="AnimationController"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "Rive", "ui_control", "rive", "flet_rive") + prop_names = [p.python_name for p in plan.properties] + assert "fit" in prop_names + assert "controller" not in prop_names + + def test_generic_controller_substring_filtered(self, analyzer): + """Any type containing 'Controller' should be filtered.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="MyWidget", + constructor_params=[ + DartParam(name="name", dart_type="String"), + DartParam(name="scroll", dart_type="ScrollController"), + DartParam(name="riveCtrl", dart_type="RiveAnimationController"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "MyWidget", "ui_control", "my_pkg", "flet_my") + prop_names = [p.python_name for p in plan.properties] + assert "name" in prop_names + assert "scroll" not in prop_names + assert "rive_ctrl" not in prop_names + + def test_painter_type_filtered(self, analyzer): + """Types ending in 'Painter' should be filtered.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="MyWidget", + constructor_params=[ + DartParam(name="enabled", dart_type="bool"), + DartParam(name="painter", dart_type="CustomPainter"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "MyWidget", "ui_control", "my_pkg", "flet_my") + prop_names = [p.python_name for p in plan.properties] + assert "enabled" in prop_names + assert "painter" not in prop_names + + +class TestRiveLikeWidgetParsing: + """End-to-end test: a rive-like package with FooWidget as main class.""" + + def test_selects_rive_widget_not_panel(self, analyzer): + """RiveWidget should be selected as main widget, not RivePanel.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="RiveWidget", + constructor_params=[ + DartParam(name="fit", dart_type="BoxFit"), + DartParam(name="alignment", dart_type="Alignment"), + DartParam(name="useSharedTexture", dart_type="bool"), + DartParam(name="layoutScaleFactor", dart_type="double"), + DartParam(name="controller", dart_type="AnimationController"), + ], + ), + DartClass( + name="RivePanel", + constructor_params=[ + DartParam(name="backgroundColor", dart_type="Color"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "Rive", "ui_control", "rive", "flet_rive") + assert plan.dart_main_class == "RiveWidget" + prop_names = [p.python_name for p in plan.properties] + assert "fit" in prop_names + assert "alignment" in prop_names + assert "use_shared_texture" in prop_names + assert "layout_scale_factor" in prop_names + # Controller should be filtered + assert "controller" not in prop_names + + def test_rive_fit_type_maps_to_ft_box_fit(self, analyzer): + """Fit type (rive-specific) should map to ft.BoxFit.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="RiveWidget", + constructor_params=[ + DartParam(name="fit", dart_type="Fit"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "Rive", "ui_control", "rive", "flet_rive") + prop = next(p for p in plan.properties if p.python_name == "fit") + assert prop.python_type == "ft.BoxFit | None" + + +class TestEnumLikeSuffixHeuristic: + """Tests that unknown types with enum-like suffixes map to str, not dict.""" + + def test_rive_hit_test_behavior_maps_to_str(self, analyzer): + """RiveHitTestBehavior (enum from transitive dep) → str | None.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="RiveWidget", + constructor_params=[ + DartParam(name="hitTestBehavior", dart_type="RiveHitTestBehavior"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "Rive", "ui_control", "rive", "flet_rive") + prop = next(p for p in plan.properties if p.python_name == "hit_test_behavior") + assert prop.python_type == "str | None" + + def test_custom_mode_enum_maps_to_str(self, analyzer): + """Types ending in 'Mode' should map to str.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="MyWidget", + constructor_params=[ + DartParam(name="renderMode", dart_type="CustomRenderMode"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "MyWidget", "ui_control", "my_pkg", "flet_my") + prop = next(p for p in plan.properties if p.python_name == "render_mode") + assert prop.python_type == "str | None" + + def test_custom_style_enum_maps_to_str(self, analyzer): + """Types ending in 'Style' should map to str.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="MyWidget", + constructor_params=[ + DartParam(name="lineStyle", dart_type="CustomLineStyle"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "MyWidget", "ui_control", "my_pkg", "flet_my") + prop = next(p for p in plan.properties if p.python_name == "line_style") + assert prop.python_type == "str | None" + + def test_unknown_class_without_enum_suffix_stays_dict(self, analyzer): + """Unknown types without enum suffix should still become dict | None.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="MyWidget", + constructor_params=[ + DartParam(name="options", dart_type="CustomOptions"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "MyWidget", "ui_control", "my_pkg", "flet_my") + prop = next(p for p in plan.properties if p.python_name == "options") + assert prop.python_type == "dict | None" + + +class TestParserFieldRegex: + """Bug 1: Ensure `return child;` is not matched as a field declaration.""" + + def test_widget_child_type_not_return(self, analyzer): + """Widget? child should map to ft.Control | None, not return | None.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="CameraPreview", + parent_class="StatelessWidget", + constructor_params=[ + DartParam(name="child", dart_type="Widget?"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "Camera", "ui_control", "camera", "flet_camera") + child_props = [p for p in plan.properties if p.python_name == "child"] + assert len(child_props) == 1 + assert "return" not in child_props[0].python_type + assert "ft.Control" in child_props[0].python_type + + +class TestDartNamePreserved: + """Bug 2: PropertyPlan.dart_name should preserve original camelCase.""" + + def test_dart_name_stored(self, analyzer): + api = DartPackageAPI( + classes=[ + DartClass( + name="WebView", + parent_class="StatefulWidget", + constructor_params=[ + DartParam(name="initialUrl", dart_type="String?"), + DartParam(name="debuggingEnabled", dart_type="bool"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "WebView", "ui_control", "webview_flutter", "flet_webview") + prop_map = {p.python_name: p for p in plan.properties} + assert prop_map["initial_url"].dart_name == "initialUrl" + assert prop_map["debugging_enabled"].dart_name == "debuggingEnabled" + + +class TestPolicySuffix: + """Bug 3: AutoMediaPlaybackPolicy should map to str, not dict.""" + + def test_policy_suffix_maps_to_str(self, analyzer): + api = DartPackageAPI( + classes=[ + DartClass( + name="WebView", + parent_class="StatefulWidget", + constructor_params=[ + DartParam( + name="initialMediaPlaybackPolicy", + dart_type="AutoMediaPlaybackPolicy", + ), + ], + ), + ] + ) + plan = analyzer.analyze(api, "WebView", "ui_control", "webview_flutter", "flet_webview") + props = {p.python_name: p for p in plan.properties} + assert "str" in props["initial_media_playback_policy"].python_type + + def test_strategy_suffix_maps_to_str(self, analyzer): + api = DartPackageAPI( + classes=[ + DartClass( + name="Foo", + parent_class="StatefulWidget", + constructor_params=[ + DartParam(name="loadStrategy", dart_type="LoadStrategy"), + ], + ), + ] + ) + plan = analyzer.analyze(api, "Foo", "ui_control", "foo", "flet_foo") + props = {p.python_name: p for p in plan.properties} + assert "str" in props["load_strategy"].python_type + + +class TestUnknownGenericOuters: + """Bug 4a: Unknown generic wrappers like Factory[...] → dict | None.""" + + def test_factory_wrapper_collapsed(self, analyzer): + api = DartPackageAPI( + classes=[ + DartClass( + name="WebView", + parent_class="StatefulWidget", + constructor_params=[ + DartParam( + name="gestureRecognizers", + dart_type="Set>?", + ), + ], + ), + ] + ) + plan = analyzer.analyze(api, "WebView", "ui_control", "webview_flutter", "flet_webview") + props = {p.python_name: p for p in plan.properties} + ptype = props["gesture_recognizers"].python_type + # Factory should NOT appear in the type + assert "Factory" not in ptype + + def test_undefined_inner_type_sanitized(self, analyzer): + """Types from barrel (known_types) not actually generated → dict.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="WebView", + parent_class="StatefulWidget", + constructor_params=[ + DartParam( + name="initialCookies", + dart_type="List", + ), + ], + ), + ] + ) + plan = analyzer.analyze(api, "WebView", "ui_control", "webview_flutter", "flet_webview") + props = {p.python_name: p for p in plan.properties} + ptype = props["initial_cookies"].python_type + # WebViewCookie is not generated as a Python class → should be dict + assert "WebViewCookie" not in ptype + assert "list[" in ptype + + +class TestFallback: + def test_empty_api(self, analyzer): + api = DartPackageAPI() + plan = analyzer.analyze(api, "Empty", "service", "empty_flutter", "flet_empty") + assert plan.main_methods == [] + assert plan.sub_modules == [] + assert plan.events == [] + + def test_single_method_class_no_submodule(self, analyzer): + """Classes with < 2 methods don't become sub-modules.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="MyMain", + methods=[DartMethod(name="doThing", return_type="void")], + ), + DartClass( + name="MyMainHelper", + methods=[DartMethod(name="help", return_type="void")], + ), + ] + ) + plan = analyzer.analyze(api, "MyMain", "service", "my_main_flutter", "flet_my_main") + # Helper has only 1 method, should NOT become a sub-module + assert len(plan.sub_modules) == 0 + # But its methods should be merged into main_methods + main_names = [m.python_name for m in plan.main_methods] + assert "do_thing" in main_names + assert "help" in main_names + + +class TestCompoundWidgetDetection: + """Tests for compound widget (sub-control) detection.""" + + def test_typed_sub_widget_detected(self, analyzer): + """A typed param like ActionPane? should become a SubControlPlan.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="Slidable", + constructor_params=[ + DartParam(name="startActionPane", dart_type="ActionPane?"), + DartParam(name="child", dart_type="Widget"), + ], + parent_class="StatefulWidget", + ), + ], + component_classes=[ + DartClass( + name="ActionPane", + constructor_params=[ + DartParam(name="extentRatio", dart_type="double"), + DartParam(name="children", dart_type="List"), + ], + ), + ], + ) + plan = analyzer.analyze(api, "Slidable", "ui_control", "flutter_slidable", "flet_slidable") + assert len(plan.sub_controls) == 1 + sc = plan.sub_controls[0] + assert sc.control_name == "ActionPane" + assert sc.parent_property == "start_action_pane" + assert sc.is_list is False + + def test_list_sub_widget_detected(self, analyzer): + """A List param should become a SubControlPlan with is_list=True.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="NavBar", + constructor_params=[ + DartParam(name="items", dart_type="List"), + ], + parent_class="StatefulWidget", + ), + ], + component_classes=[ + DartClass( + name="BarItem", + constructor_params=[ + DartParam(name="title", dart_type="String"), + ], + ), + ], + ) + plan = analyzer.analyze(api, "NavBar", "ui_control", "nav_bar", "flet_nav_bar") + assert len(plan.sub_controls) == 1 + sc = plan.sub_controls[0] + assert sc.control_name == "BarItem" + assert sc.is_list is True + assert sc.parent_property == "items" + + def test_plain_widget_not_sub_control(self, analyzer): + """A Widget? child should remain ft.Control, not a SubControlPlan.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="MyWidget", + constructor_params=[ + DartParam(name="child", dart_type="Widget?"), + DartParam(name="title", dart_type="String"), + ], + parent_class="StatefulWidget", + ), + ], + ) + plan = analyzer.analyze(api, "MyWidget", "ui_control", "my_widget", "flet_my_widget") + assert len(plan.sub_controls) == 0 + # child should map to ft.Control | None, not a sub-control + child_prop = next((p for p in plan.properties if p.python_name == "child"), None) + assert child_prop is not None + assert "ft.Control" in child_prop.python_type + + def test_max_depth_respected(self, analyzer): + """Nesting deeper than max_depth=3 should not recurse.""" + # Create a 4-level deep chain: A → B → C → D + api = DartPackageAPI( + classes=[ + DartClass( + name="A", + constructor_params=[ + DartParam(name="bChild", dart_type="B?"), + ], + parent_class="StatefulWidget", + ), + ], + component_classes=[ + DartClass( + name="B", + constructor_params=[DartParam(name="cChild", dart_type="C?")], + ), + DartClass( + name="C", + constructor_params=[DartParam(name="dChild", dart_type="D?")], + ), + DartClass( + name="D", + constructor_params=[DartParam(name="value", dart_type="int")], + ), + ], + ) + plan = analyzer.analyze(api, "A", "ui_control", "a_pkg", "flet_a") + # A → B (depth 1) → C (depth 2) → D (depth 3, max) + assert len(plan.sub_controls) == 1 # B + b = plan.sub_controls[0] + assert b.control_name == "B" + assert len(b.sub_controls) == 1 # C + c = b.sub_controls[0] + assert c.control_name == "C" + assert len(c.sub_controls) == 1 # D + d = c.sub_controls[0] + assert d.control_name == "D" + assert len(d.sub_controls) == 0 # depth 3 = max, no further nesting + + def test_external_type_not_sub_control(self, analyzer): + """Types not in the same package should NOT become sub-controls.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="MyWidget", + constructor_params=[ + DartParam(name="data", dart_type="ExternalClass?"), + ], + parent_class="StatefulWidget", + ), + ], + # No component_classes — ExternalClass is from another package + ) + plan = analyzer.analyze(api, "MyWidget", "ui_control", "my_widget", "flet_my_widget") + assert len(plan.sub_controls) == 0 + + def test_backward_compat_simple_widget(self, analyzer): + """Simple widget without sub-controls should have no regression.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="CameraPreview", + constructor_params=[ + DartParam(name="child", dart_type="Widget?"), + DartParam(name="fit", dart_type="BoxFit?"), + DartParam(name="width", dart_type="double"), + ], + parent_class="StatefulWidget", + ), + ], + ) + plan = analyzer.analyze(api, "CameraPreview", "ui_control", "camera", "flet_camera") + assert len(plan.sub_controls) == 0 + assert len(plan.properties) > 0 + prop_names = [p.python_name for p in plan.properties] + assert "fit" in prop_names + assert "width" in prop_names + + def test_sub_control_property_type_not_sanitized(self, analyzer): + """Sub-control types should appear as class names, not dict | None.""" + api = DartPackageAPI( + classes=[ + DartClass( + name="Slidable", + constructor_params=[ + DartParam(name="actionPane", dart_type="ActionPane?"), + DartParam(name="child", dart_type="Widget"), + ], + parent_class="StatefulWidget", + ), + ], + component_classes=[ + DartClass( + name="ActionPane", + constructor_params=[ + DartParam(name="extentRatio", dart_type="double"), + ], + ), + ], + ) + plan = analyzer.analyze(api, "Slidable", "ui_control", "flutter_slidable", "flet_slidable") + # The property type should be ActionPane | None, not dict | None + ap_prop = next((p for p in plan.properties if p.python_name == "action_pane"), None) + assert ap_prop is not None + assert "ActionPane" in ap_prop.python_type + assert "dict" not in ap_prop.python_type + + +# --------------------------------------------------------------------------- +# Multi-widget classification tests +# --------------------------------------------------------------------------- + + +class TestMultiWidgetClassification: + """Tests for _classify_multi_widgets and multi-widget processing.""" + + def _make_widget(self, name: str, params: list[str]) -> DartClass: + """Helper to make a DartClass with constructor params.""" + return DartClass( + name=name, + constructor_params=[DartParam(name=p, dart_type="double") for p in params], + ) + + def test_single_widget_returns_single(self, analyzer): + widgets = [self._make_widget("Shimmer", ["color", "size"])] + strategy, filtered = analyzer._classify_multi_widgets(widgets, "Shimmer") + assert strategy == "single" + assert len(filtered) == 1 + + def test_family_detection_many_widgets(self, analyzer): + """≥5 widgets should classify as family.""" + shared = ["color", "size", "duration"] + widgets = [ + self._make_widget(f"SpinKit{name}", shared + [f"extra_{i}"]) + for i, name in enumerate(["Circle", "HourGlass", "Wave", "Pulse", "Ring"]) + ] + strategy, filtered = analyzer._classify_multi_widgets(widgets, "SpinKit") + assert strategy == "family" + assert len(filtered) == 5 + + def test_family_detection_high_overlap(self, analyzer): + """2-4 widgets with ≥60% overlap should classify as family.""" + widgets = [ + self._make_widget("FooA", ["color", "size", "duration", "extra"]), + self._make_widget("FooB", ["color", "size", "duration", "other"]), + ] + strategy, _ = analyzer._classify_multi_widgets(widgets, "Foo") + assert strategy == "family" + + def test_sibling_detection_low_overlap(self, analyzer): + """2-4 widgets with <60% overlap should classify as sibling.""" + widgets = [ + self._make_widget("CircularIndicator", ["percent", "radius", "center"]), + self._make_widget( + "LinearIndicator", ["percent", "width", "height", "bar_radius", "fill"] + ), + ] + strategy, _ = analyzer._classify_multi_widgets(widgets, "Indicator") + assert strategy == "sibling" + + def test_single_fallback_private_only(self, analyzer): + """All private classes → fallback to single.""" + widgets = [ + self._make_widget("_InternalWidget", ["x"]), + ] + strategy, _ = analyzer._classify_multi_widgets(widgets, "Internal") + assert strategy == "single" + + +class TestWidgetFamilyProcessing: + """Tests for _process_widget_family.""" + + def _make_widget(self, name: str, params: list[str]) -> DartClass: + return DartClass( + name=name, + constructor_params=[DartParam(name=p, dart_type="double") for p in params], + ) + + def test_family_creates_type_enum(self, analyzer): + shared = ["color", "size"] + widgets = [ + self._make_widget(f"SpinKit{v}", shared) + for v in ["Circle", "HourGlass", "Wave", "Pulse", "Ring"] + ] + api = DartPackageAPI(classes=widgets) + plan = analyzer.analyze(api, "SpinKit", "ui_control", "flutter_spinkit", "flet_spinkit") + # Should have SpinKitType enum + enum_names = [e.python_name for e in plan.enums] + assert "SpinKitType" in enum_names + + # Should have widget_family_variants + assert len(plan.widget_family_variants) == 5 + variant_values = [v.enum_value for v in plan.widget_family_variants] + assert "circle" in variant_values + assert "hour_glass" in variant_values + + def test_family_extracts_shared_properties(self, analyzer): + widgets = [ + self._make_widget("SpinKitCircle", ["color", "size", "lineWidth"]), + self._make_widget("SpinKitWave", ["color", "size", "itemCount"]), + ] + api = DartPackageAPI(classes=widgets) + plan = analyzer.analyze(api, "SpinKit", "ui_control", "flutter_spinkit", "flet_spinkit") + prop_names = [p.python_name for p in plan.properties] + assert "color" in prop_names + assert "size" in prop_names + # "type" should be the first property + assert prop_names[0] == "type" + + def test_family_has_type_property(self, analyzer): + widgets = [ + self._make_widget("SpinKitA", ["x"]), + self._make_widget("SpinKitB", ["x"]), + self._make_widget("SpinKitC", ["x"]), + self._make_widget("SpinKitD", ["x"]), + self._make_widget("SpinKitE", ["x"]), + ] + api = DartPackageAPI(classes=widgets) + plan = analyzer.analyze(api, "SpinKit", "ui_control", "flutter_spinkit", "flet_spinkit") + # First property should be "type" + assert plan.properties[0].python_name == "type" + assert "SpinKitType" in plan.properties[0].python_type + + +class TestSiblingWidgetProcessing: + """Tests for _process_sibling_widgets.""" + + def _make_widget(self, name: str, params: list[str]) -> DartClass: + return DartClass( + name=name, + constructor_params=[DartParam(name=p, dart_type="double") for p in params], + ) + + def test_sibling_creates_separate_plans(self, analyzer): + widgets = [ + self._make_widget("CircularPercentIndicator", ["percent", "radius"]), + self._make_widget("LinearPercentIndicator", ["percent", "width", "height", "extra"]), + ] + api = DartPackageAPI(classes=widgets) + plan = analyzer.analyze( + api, "PercentIndicator", "ui_control", "percent_indicator", "flet_percent_indicator" + ) + # Should have sibling_widgets + assert len(plan.sibling_widgets) >= 1 + sib_names = [s.control_name for s in plan.sibling_widgets] + # One of the two is the main widget, the other is a sibling + assert plan.dart_main_class in ("CircularPercentIndicator", "LinearPercentIndicator") + # The sibling should be the other one + for sib_name in sib_names: + assert sib_name != plan.dart_main_class + + def test_sibling_has_own_properties(self, analyzer): + widgets = [ + self._make_widget("CircularIndicator", ["percent", "radius"]), + self._make_widget("LinearIndicator", ["percent", "width", "height"]), + ] + api = DartPackageAPI(classes=widgets) + plan = analyzer.analyze(api, "Indicator", "ui_control", "test_indicator", "flet_indicator") + assert len(plan.sibling_widgets) >= 1 + sibling = plan.sibling_widgets[0] + assert len(sibling.properties) > 0 diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..2ddf68f --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,84 @@ +import re +from unittest.mock import patch + +from typer.testing import CliRunner + +from flet_pkg import __version__ +from flet_pkg.main import app + +runner = CliRunner() + +_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m") + + +def _strip_ansi(text: str) -> str: + return _ANSI_RE.sub("", text) + + +def _no_registry_check(project_name: str) -> None: + """Stub that skips the PyPI/GitHub check during tests.""" + + +class TestCLI: + def test_version(self): + result = runner.invoke(app, ["--version"]) + assert result.exit_code == 0 + assert __version__ in result.output + + def test_help(self): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "create" in result.output + + def test_create_help(self): + result = runner.invoke(app, ["create", "--help"]) + assert result.exit_code == 0 + output = _strip_ansi(result.output) + assert "--type" in output + assert "--flutter-package" in output + + def test_create_invalid_type(self): + result = runner.invoke(app, ["create", "--type", "invalid"]) + assert result.exit_code == 1 + + @patch("flet_pkg.commands.create._check_existing_packages", _no_registry_check) + def test_create_service_non_interactive(self, tmp_path): + result = runner.invoke( + app, + [ + "create", + "--type", + "service", + "--flutter-package", + "onesignal_flutter", + "--output", + str(tmp_path), + "--no-ai-refine", + ], + input="flet-onesignal\nflet_onesignal\nOneSignal\nA test\nAuthor\ny\n", + ) + assert result.exit_code == 0 + assert (tmp_path / "flet-onesignal").is_dir() + assert (tmp_path / "flet-onesignal" / "pyproject.toml").is_file() + + @patch("flet_pkg.commands.create._check_existing_packages", _no_registry_check) + def test_create_ui_control_non_interactive(self, tmp_path): + result = runner.invoke( + app, + [ + "create", + "--type", + "ui_control", + "--flutter-package", + "flutter_spinkit", + "--output", + str(tmp_path), + "--no-ai-refine", + ], + input="flet-spinkit\nflet_spinkit\nSpinkit\nA widget\nAuthor\ny\n", + ) + assert result.exit_code == 0 + assert (tmp_path / "flet-spinkit").is_dir() + control = tmp_path / "flet-spinkit" / "src" / "flet_spinkit" / "spinkit.py" + assert control.is_file() + assert "LayoutControl" in control.read_text() diff --git a/tests/test_downloader.py b/tests/test_downloader.py new file mode 100644 index 0000000..ca59492 --- /dev/null +++ b/tests/test_downloader.py @@ -0,0 +1,86 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from flet_pkg.core.downloader import ( + DownloadError, + PackageMetadata, + PackageNotFoundError, + PubDevDownloader, +) + + +@pytest.fixture +def downloader(tmp_path): + return PubDevDownloader(cache_dir=tmp_path / "cache") + + +class TestFetchMetadata: + def test_success(self, downloader): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "latest": { + "version": "5.2.0", + "pubspec": { + "description": "OneSignal Flutter SDK", + "homepage": "https://onesignal.com", + "repository": "https://github.com/OneSignal/OneSignal-Flutter-SDK", + }, + } + } + + with patch("flet_pkg.core.downloader.httpx.get", return_value=mock_response): + meta = downloader.fetch_metadata("onesignal_flutter") + + assert isinstance(meta, PackageMetadata) + assert meta.name == "onesignal_flutter" + assert meta.version == "5.2.0" + assert meta.description == "OneSignal Flutter SDK" + + def test_not_found(self, downloader): + mock_response = MagicMock() + mock_response.status_code = 404 + + with patch("flet_pkg.core.downloader.httpx.get", return_value=mock_response): + with pytest.raises(PackageNotFoundError): + downloader.fetch_metadata("nonexistent_package_xyz") + + def test_server_error(self, downloader): + mock_response = MagicMock() + mock_response.status_code = 500 + + with patch("flet_pkg.core.downloader.httpx.get", return_value=mock_response): + with pytest.raises(DownloadError): + downloader.fetch_metadata("some_package") + + +class TestDownload: + def test_cache_hit(self, downloader, tmp_path): + cache_path = tmp_path / "cache" / "test_pkg-1.0.0" + cache_path.mkdir(parents=True) + (cache_path / "lib").mkdir() + (cache_path / "lib" / "test.dart").write_text("// cached") + + result = downloader.download("test_pkg", version="1.0.0") + assert result == cache_path + assert (result / "lib").exists() + + def test_resolves_latest_version(self, downloader): + meta_response = MagicMock() + meta_response.status_code = 200 + meta_response.json.return_value = { + "latest": { + "version": "2.0.0", + "pubspec": {"description": "Test"}, + } + } + + download_response = MagicMock() + download_response.status_code = 404 + + with patch( + "flet_pkg.core.downloader.httpx.get", side_effect=[meta_response, download_response] + ): + with pytest.raises(PackageNotFoundError): + downloader.download("test_pkg") diff --git a/tests/test_generators.py b/tests/test_generators.py new file mode 100644 index 0000000..639f13f --- /dev/null +++ b/tests/test_generators.py @@ -0,0 +1,741 @@ +import pytest + +from flet_pkg.core.generators import ( + DartServiceGenerator, + PythonControlGenerator, + PythonInitGenerator, + PythonSubModuleGenerator, + PythonTypesGenerator, +) +from flet_pkg.core.models import ( + EnumPlan, + EventPlan, + GenerationPlan, + MethodPlan, + ParamPlan, + PropertyPlan, + SiblingWidgetPlan, + SubControlPlan, + SubModulePlan, + WidgetVariant, +) + + +@pytest.fixture +def sample_plan(): + """A full GenerationPlan for testing generators.""" + return GenerationPlan( + control_name="OneSignal", + package_name="flet_onesignal", + base_class="ft.Service", + description="OneSignal integration for Flet.", + flutter_package="onesignal_flutter", + dart_import="package:onesignal_flutter/onesignal_flutter.dart", + properties=[ + PropertyPlan( + python_name="app_id", python_type="str", default_value='""', docstring="App ID." + ), + ], + main_methods=[ + MethodPlan( + python_name="login", + dart_method_name="login", + params=[ + ParamPlan(python_name="external_id", python_type="str", dart_name="external_id") + ], + return_type="None", + is_async=True, + docstring="Login with external ID.", + ), + MethodPlan( + python_name="logout", + dart_method_name="logout", + return_type="None", + is_async=True, + ), + ], + events=[ + EventPlan( + python_attr_name="on_click", + event_class_name="OneSignalClickEvent", + dart_event_name="click", + fields=[("data", "dict")], + ), + ], + sub_modules=[ + SubModulePlan( + module_name="user", + class_name="OneSignalUser", + dart_prefix="user", + methods=[ + MethodPlan( + python_name="add_tag", + dart_method_name="user_add_tag", + params=[ + ParamPlan(python_name="key", python_type="str", dart_name="key"), + ParamPlan(python_name="value", python_type="str", dart_name="value"), + ], + return_type="None", + is_async=True, + ), + MethodPlan( + python_name="get_tags", + dart_method_name="user_get_tags", + return_type="dict[str, str]", + is_async=True, + ), + ], + docstring="User management.", + ), + ], + enums=[ + EnumPlan( + python_name="OSLogLevel", + values=[("NONE", "none", ""), ("DEBUG", "debug", ""), ("INFO", "info", "")], + docstring="Log levels.", + ), + ], + dart_listeners=[ + {"event_name": "click", "python_attr": "on_click"}, + ], + ) + + +class TestPythonControlGenerator: + def test_generates_file(self, sample_plan): + gen = PythonControlGenerator() + files = gen.generate(sample_plan) + assert "one_signal.py" in files + + def test_contains_class_definition(self, sample_plan): + gen = PythonControlGenerator() + files = gen.generate(sample_plan) + content = files["one_signal.py"] + assert '@ft.control("OneSignal")' in content + assert "class OneSignal(ft.Service):" in content + + def test_contains_imports(self, sample_plan): + gen = PythonControlGenerator() + files = gen.generate(sample_plan) + content = files["one_signal.py"] + assert "import flet as ft" in content + assert "from flet_onesignal.user import OneSignalUser" in content + + def test_contains_properties(self, sample_plan): + gen = PythonControlGenerator() + files = gen.generate(sample_plan) + content = files["one_signal.py"] + assert 'app_id: str = ""' in content + + def test_contains_events(self, sample_plan): + gen = PythonControlGenerator() + files = gen.generate(sample_plan) + content = files["one_signal.py"] + assert "on_click: Optional[ft.EventHandler[OneSignalClickEvent]]" in content + + def test_contains_methods(self, sample_plan): + gen = PythonControlGenerator() + files = gen.generate(sample_plan) + content = files["one_signal.py"] + assert "async def login(self, external_id: str)" in content + assert "async def logout(self)" in content + + def test_contains_submodule_property(self, sample_plan): + gen = PythonControlGenerator() + files = gen.generate(sample_plan) + content = files["one_signal.py"] + assert "def user(self) -> OneSignalUser:" in content + + def test_contains_invoke_method(self, sample_plan): + gen = PythonControlGenerator() + files = gen.generate(sample_plan) + content = files["one_signal.py"] + assert "_invoke_method" in content + + +class TestPythonSubModuleGenerator: + def test_generates_files(self, sample_plan): + gen = PythonSubModuleGenerator() + files = gen.generate(sample_plan) + assert "user.py" in files + + def test_contains_class(self, sample_plan): + gen = PythonSubModuleGenerator() + files = gen.generate(sample_plan) + content = files["user.py"] + assert "class OneSignalUser:" in content + + def test_contains_methods(self, sample_plan): + gen = PythonSubModuleGenerator() + files = gen.generate(sample_plan) + content = files["user.py"] + assert "async def add_tag(self, key: str, value: str)" in content + assert '"user_add_tag"' in content + + def test_contains_type_checking(self, sample_plan): + gen = PythonSubModuleGenerator() + files = gen.generate(sample_plan) + content = files["user.py"] + assert "TYPE_CHECKING" in content + + +class TestPythonTypesGenerator: + def test_generates_types_file(self, sample_plan): + gen = PythonTypesGenerator() + files = gen.generate(sample_plan) + assert "types.py" in files + + def test_contains_enum(self, sample_plan): + gen = PythonTypesGenerator() + files = gen.generate(sample_plan) + content = files["types.py"] + assert "class OSLogLevel(Enum):" in content + assert 'NONE = "none"' in content + + def test_contains_event_class(self, sample_plan): + gen = PythonTypesGenerator() + files = gen.generate(sample_plan) + content = files["types.py"] + assert "class OneSignalClickEvent(ft.Event" in content + + def test_contains_error_event(self, sample_plan): + gen = PythonTypesGenerator() + files = gen.generate(sample_plan) + content = files["types.py"] + assert "class OneSignalErrorEvent(ft.Event" in content + + def test_empty_plan_still_generates_error_event(self): + plan = GenerationPlan(control_name="X", package_name="x") + gen = PythonTypesGenerator() + files = gen.generate(plan) + # types.py is always generated (error event class is always needed) + assert "types.py" in files + assert "XErrorEvent" in files["types.py"] + + +class TestPythonInitGenerator: + def test_generates_init(self, sample_plan): + gen = PythonInitGenerator() + files = gen.generate(sample_plan) + assert "__init__.py" in files + + def test_contains_main_import(self, sample_plan): + gen = PythonInitGenerator() + files = gen.generate(sample_plan) + content = files["__init__.py"] + assert "from flet_onesignal.one_signal import OneSignal" in content + + def test_contains_all(self, sample_plan): + gen = PythonInitGenerator() + files = gen.generate(sample_plan) + content = files["__init__.py"] + assert "__all__" in content + assert '"OneSignal"' in content + + +class TestDartServiceGenerator: + def test_generates_dart_file(self, sample_plan): + gen = DartServiceGenerator() + files = gen.generate(sample_plan) + assert "one_signal_service.dart" in files + + def test_contains_class(self, sample_plan): + gen = DartServiceGenerator() + files = gen.generate(sample_plan) + content = files["one_signal_service.dart"] + assert "class OneSignalService extends FletService" in content + + def test_contains_switch(self, sample_plan): + gen = DartServiceGenerator() + files = gen.generate(sample_plan) + content = files["one_signal_service.dart"] + assert '"login"' in content + assert '"logout"' in content + assert '"user_add_tag"' in content + + def test_contains_error_handler(self, sample_plan): + gen = DartServiceGenerator() + files = gen.generate(sample_plan) + content = files["one_signal_service.dart"] + assert "_handleError" in content + + def test_contains_import(self, sample_plan): + gen = DartServiceGenerator() + files = gen.generate(sample_plan) + content = files["one_signal_service.dart"] + assert "package:onesignal_flutter/onesignal_flutter.dart" in content + + +class TestDartUIControlGenerator: + """Tests for StatefulWidget generation for UI controls.""" + + @pytest.fixture + def ui_plan(self): + return GenerationPlan( + control_name="Rive", + package_name="flet_rive", + base_class="ft.LayoutControl", + flutter_package="rive", + dart_import="package:rive/rive.dart", + dart_main_class="RiveAnimation", + properties=[ + PropertyPlan( + python_name="fit", + python_type="ft.BoxFit | None", + default_value="None", + dart_getter='control.getBoxFit("fit")', + ), + PropertyPlan( + python_name="alignment", + python_type="ft.Alignment | None", + default_value="None", + dart_getter='control.getAlignment("alignment")', + ), + PropertyPlan( + python_name="antialiasing", + python_type="bool", + default_value="True", + dart_getter='control.getBool("antialiasing", false)!', + ), + ], + events=[ + EventPlan( + python_attr_name="on_init", + event_class_name="RiveOnInitEvent", + dart_event_name="init", + ), + ], + ) + + def test_generates_widget_file(self, ui_plan): + gen = DartServiceGenerator() + files = gen.generate(ui_plan) + assert "rive_widget.dart" in files + + def test_contains_stateful_widget(self, ui_plan): + gen = DartServiceGenerator() + files = gen.generate(ui_plan) + content = files["rive_widget.dart"] + assert "class RiveWidget extends StatefulWidget" in content + + def test_contains_state_class(self, ui_plan): + gen = DartServiceGenerator() + files = gen.generate(ui_plan) + content = files["rive_widget.dart"] + assert "class _RiveWidgetState extends State" in content + + def test_contains_layout_control(self, ui_plan): + gen = DartServiceGenerator() + files = gen.generate(ui_plan) + content = files["rive_widget.dart"] + assert "LayoutControl(" in content + assert "control: widget.control," in content + + def test_contains_sdk_widget(self, ui_plan): + gen = DartServiceGenerator() + files = gen.generate(ui_plan) + content = files["rive_widget.dart"] + assert "RiveAnimation(" in content + + def test_contains_typed_getters(self, ui_plan): + gen = DartServiceGenerator() + files = gen.generate(ui_plan) + content = files["rive_widget.dart"] + assert "getBoxFit" in content + assert "getAlignment" in content + assert "getBool" in content + + def test_contains_error_handler(self, ui_plan): + gen = DartServiceGenerator() + files = gen.generate(ui_plan) + content = files["rive_widget.dart"] + assert "_handleError" in content + assert 'triggerEvent("error"' in content + + def test_contains_flutter_widgets_import(self, ui_plan): + gen = DartServiceGenerator() + files = gen.generate(ui_plan) + content = files["rive_widget.dart"] + assert "package:flutter/widgets.dart" in content + + def test_no_flet_widget_base_class(self, ui_plan): + gen = DartServiceGenerator() + files = gen.generate(ui_plan) + content = files["rive_widget.dart"] + assert "FletWidget" not in content + + def test_dart_constructor_uses_camel_case_names(self): + """Bug 2: Dart constructor args must use camelCase, not snake_case.""" + plan = GenerationPlan( + control_name="WebView", + package_name="flet_webview", + base_class="ft.LayoutControl", + flutter_package="webview_flutter", + dart_import="package:webview_flutter/webview_flutter.dart", + dart_main_class="WebView", + properties=[ + PropertyPlan( + python_name="initial_url", + python_type="str | None", + default_value="None", + dart_name="initialUrl", + ), + PropertyPlan( + python_name="debugging_enabled", + python_type="bool", + default_value="False", + dart_name="debuggingEnabled", + dart_getter='widget.control.getBool("debugging_enabled", false)!', + ), + ], + ) + gen = DartServiceGenerator() + files = gen.generate(plan) + content = files["web_view_widget.dart"] + # Constructor args must use camelCase Dart names + assert "initialUrl: initialUrl," in content + assert "debuggingEnabled: debuggingEnabled," in content + # Must NOT use snake_case + assert "initial_url:" not in content + assert "debugging_enabled:" not in content + + +class TestPythonControlFieldImport: + """Tests for field(default_factory) import in python_control.py.""" + + def test_field_import_for_list_property(self): + plan = GenerationPlan( + control_name="MyWidget", + package_name="flet_my_widget", + base_class="ft.LayoutControl", + properties=[ + PropertyPlan( + python_name="items", + python_type="list[str]", + default_value="field(default_factory=list)", + ), + ], + ) + gen = PythonControlGenerator() + files = gen.generate(plan) + content = files["my_widget.py"] + assert "from dataclasses import field" in content + + def test_no_field_import_without_list_or_submodules(self): + plan = GenerationPlan( + control_name="MyWidget", + package_name="flet_my_widget", + base_class="ft.LayoutControl", + properties=[ + PropertyPlan(python_name="name", python_type="str", default_value='""'), + ], + ) + gen = PythonControlGenerator() + files = gen.generate(plan) + content = files["my_widget.py"] + assert "from dataclasses import field" not in content + + +class TestSubControlGeneration: + """Tests for compound widget (sub-control) code generation.""" + + @pytest.fixture + def slidable_plan(self): + """A GenerationPlan with sub-controls (flutter_slidable-like).""" + return GenerationPlan( + control_name="Slidable", + package_name="flet_slidable", + base_class="ft.LayoutControl", + flutter_package="flutter_slidable", + dart_import="package:flutter_slidable/flutter_slidable.dart", + dart_main_class="Slidable", + properties=[ + PropertyPlan( + python_name="start_action_pane", + python_type="ActionPane | None", + default_value="None", + dart_name="startActionPane", + dart_getter='buildWidget("start_action_pane")', + ), + PropertyPlan( + python_name="child", + python_type="ft.Control | None", + default_value="None", + dart_name="child", + dart_getter='buildWidget("child")', + ), + PropertyPlan( + python_name="enabled", + python_type="bool", + default_value="True", + dart_name="enabled", + dart_getter='control.getBool("enabled", false)!', + ), + ], + sub_controls=[ + SubControlPlan( + control_name="ActionPane", + dart_class_name="ActionPane", + properties=[ + PropertyPlan( + python_name="extent_ratio", + python_type="ft.Number | None", + default_value="None", + dart_name="extentRatio", + dart_getter='control.getDouble("extent_ratio")', + ), + PropertyPlan( + python_name="children", + python_type="list[ft.Control]", + default_value="field(default_factory=list)", + dart_name="children", + dart_getter='buildWidgets("children")', + ), + ], + parent_property="start_action_pane", + is_list=False, + depth=1, + ), + ], + ) + + def test_python_sub_control_class(self, slidable_plan): + """Sub-control class should be generated with @ft.control decorator.""" + gen = PythonControlGenerator() + files = gen.generate(slidable_plan) + content = files["slidable.py"] + assert '@ft.control("ActionPane")' in content + assert "class ActionPane(ft.Control):" in content + assert "extent_ratio:" in content + + def test_python_sub_control_before_main(self, slidable_plan): + """Sub-control class should appear before the main class.""" + gen = PythonControlGenerator() + files = gen.generate(slidable_plan) + content = files["slidable.py"] + sub_pos = content.index("class ActionPane") + main_pos = content.index("class Slidable") + assert sub_pos < main_pos + + def test_dart_sub_control_widget(self, slidable_plan): + """Dart StatefulWidget should be generated for sub-control.""" + gen = DartServiceGenerator() + files = gen.generate(slidable_plan) + content = files["slidable_widget.dart"] + assert "class ActionPaneWidget extends StatefulWidget" in content + assert "_ActionPaneWidgetState" in content + assert "ActionPane(" in content + + def test_init_exports_sub_controls(self, slidable_plan): + """__init__.py should export sub-control classes.""" + gen = PythonInitGenerator() + files = gen.generate(slidable_plan) + content = files["__init__.py"] + assert "from flet_slidable.slidable import ActionPane" in content + assert '"ActionPane"' in content + + +# --------------------------------------------------------------------------- +# Widget Family generation tests +# --------------------------------------------------------------------------- + + +class TestWidgetFamilyGeneration: + """Tests for family variant code generation.""" + + @pytest.fixture + def family_plan(self): + return GenerationPlan( + control_name="SpinKit", + package_name="flet_spinkit", + base_class="ft.LayoutControl", + flutter_package="flutter_spinkit", + dart_import="package:flutter_spinkit/flutter_spinkit.dart", + dart_main_class="SpinKitCircle", + properties=[ + PropertyPlan( + python_name="type", + python_type="SpinKitType | None", + default_value="None", + dart_name="type", + dart_getter='widget.control.getString("type")', + ), + PropertyPlan( + python_name="color", + python_type="ft.Color | None", + default_value="None", + dart_name="color", + dart_getter='widget.control.getColor("color")', + ), + PropertyPlan( + python_name="size", + python_type="ft.Number | None", + default_value="None", + dart_name="size", + dart_getter='widget.control.getDouble("size")', + ), + ], + enums=[ + EnumPlan( + python_name="SpinKitType", + values=[ + ("CIRCLE", "circle", "SpinKitCircle variant."), + ("HOUR_GLASS", "hour_glass", "SpinKitHourGlass variant."), + ("WAVE", "wave", "SpinKitWave variant."), + ], + ), + ], + widget_family_variants=[ + WidgetVariant(dart_class_name="SpinKitCircle", enum_value="circle"), + WidgetVariant(dart_class_name="SpinKitHourGlass", enum_value="hour_glass"), + WidgetVariant(dart_class_name="SpinKitWave", enum_value="wave"), + ], + ) + + def test_dart_family_switch_all_variants(self, family_plan): + """Dart file should contain a switch with all variant cases.""" + gen = DartServiceGenerator() + files = gen.generate(family_plan) + content = files["spin_kit_widget.dart"] + assert 'case "circle":' in content + assert 'case "hour_glass":' in content + assert 'case "wave":' in content + assert "SpinKitCircle(" in content + assert "SpinKitHourGlass(" in content + assert "SpinKitWave(" in content + + def test_dart_family_default_case(self, family_plan): + """Dart switch should have a default case.""" + gen = DartServiceGenerator() + files = gen.generate(family_plan) + content = files["spin_kit_widget.dart"] + assert "default:" in content + + def test_dart_family_shared_args(self, family_plan): + """Each variant should receive the shared args.""" + gen = DartServiceGenerator() + files = gen.generate(family_plan) + content = files["spin_kit_widget.dart"] + assert "color: color" in content + assert "size: size" in content + + def test_python_family_type_property(self, family_plan): + """Python control should have a 'type' property.""" + gen = PythonControlGenerator() + files = gen.generate(family_plan) + content = files["spin_kit.py"] + assert "type: SpinKitType | None = None" in content + + +# --------------------------------------------------------------------------- +# Sibling widget generation tests +# --------------------------------------------------------------------------- + + +class TestSiblingWidgetGeneration: + """Tests for sibling widget code generation.""" + + @pytest.fixture + def sibling_plan(self): + return GenerationPlan( + control_name="CircularPercentIndicator", + package_name="flet_percent_indicator", + base_class="ft.LayoutControl", + flutter_package="percent_indicator", + dart_import="package:percent_indicator/percent_indicator.dart", + dart_main_class="CircularPercentIndicator", + properties=[ + PropertyPlan( + python_name="percent", + python_type="ft.Number | None", + default_value="None", + dart_name="percent", + dart_getter='widget.control.getDouble("percent")', + ), + PropertyPlan( + python_name="radius", + python_type="ft.Number | None", + default_value="None", + dart_name="radius", + dart_getter='widget.control.getDouble("radius")', + ), + ], + sibling_widgets=[ + SiblingWidgetPlan( + control_name="LinearPercentIndicator", + dart_class_name="LinearPercentIndicator", + control_name_snake="linear_percent_indicator", + properties=[ + PropertyPlan( + python_name="percent", + python_type="ft.Number | None", + default_value="None", + dart_name="percent", + dart_getter='widget.control.getDouble("percent")', + ), + PropertyPlan( + python_name="width", + python_type="ft.Number | None", + default_value="None", + dart_name="width", + dart_getter='widget.control.getDouble("width")', + ), + ], + events=[ + EventPlan( + python_attr_name="on_animation_end", + event_class_name="LinearPercentIndicatorAnimationEndEvent", + dart_event_name="animation_end", + ), + ], + ), + ], + ) + + def test_sibling_generates_separate_python_files(self, sibling_plan): + """Each sibling should get its own Python file.""" + gen = PythonControlGenerator() + files = gen.generate(sibling_plan) + assert "linear_percent_indicator.py" in files + content = files["linear_percent_indicator.py"] + assert "class LinearPercentIndicator(ft.LayoutControl):" in content + assert "percent:" in content + assert "width:" in content + + def test_sibling_generates_separate_dart_files(self, sibling_plan): + """Each sibling should get its own Dart widget file.""" + gen = DartServiceGenerator() + files = gen.generate(sibling_plan) + assert "linear_percent_indicator_widget.dart" in files + content = files["linear_percent_indicator_widget.dart"] + assert "class LinearPercentIndicatorWidget extends StatefulWidget" in content + assert "LinearPercentIndicator(" in content + + def test_sibling_extension_dart_registers_all(self, sibling_plan): + """extension.dart should register both main and sibling widgets.""" + gen = DartServiceGenerator() + files = gen.generate(sibling_plan) + assert "extension.dart" in files + content = files["extension.dart"] + assert '"CircularPercentIndicator"' in content + assert '"LinearPercentIndicator"' in content + assert "CircularPercentIndicatorWidget(control: control)" in content + assert "LinearPercentIndicatorWidget(control: control)" in content + + def test_sibling_init_exports_all(self, sibling_plan): + """__init__.py should export all sibling controls.""" + gen = PythonInitGenerator() + files = gen.generate(sibling_plan) + content = files["__init__.py"] + assert ( + "from flet_percent_indicator.linear_percent_indicator import LinearPercentIndicator" + in content + ) + assert '"LinearPercentIndicator"' in content + + def test_sibling_types_include_events(self, sibling_plan): + """types.py should include sibling event classes.""" + gen = PythonTypesGenerator() + files = gen.generate(sibling_plan) + content = files["types.py"] + assert "class LinearPercentIndicatorAnimationEndEvent" in content diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py new file mode 100644 index 0000000..dd06907 --- /dev/null +++ b/tests/test_mcp_server.py @@ -0,0 +1,243 @@ +"""Tests for the flet-pkg MCP server.""" + +from __future__ import annotations + +import pytest + +mcp_mod = pytest.importorskip("mcp") # noqa: F841 — skip all tests if mcp not installed + +from pathlib import Path # noqa: E402 + +from flet_pkg.mcp._serializers import to_dict # noqa: E402 +from flet_pkg.mcp.server import ( # noqa: E402 + mcp, + tool_derive_names, + tool_map_dart_type, + tool_run_pipeline, + tool_scaffold, +) + +FIXTURES = Path(__file__).parent / "fixtures" +ONESIGNAL_FIXTURE = FIXTURES / "onesignal_flutter" + + +# --------------------------------------------------------------------------- +# _serializers.to_dict +# --------------------------------------------------------------------------- + + +class TestToDict: + def test_dataclass(self): + from flet_pkg.core.validators import DerivedNames + + obj = DerivedNames("flet-foo", "flet_foo", "Foo", "foo") + result = to_dict(obj) + assert result == { + "project_name": "flet-foo", + "package_name": "flet_foo", + "control_name": "Foo", + "control_name_snake": "foo", + } + + def test_path(self): + assert to_dict(Path("/tmp/foo")) == "/tmp/foo" + + def test_enum(self): + from flet_pkg.core.ai.models import GapKind + + assert to_dict(GapKind.MISSING_METHOD) == "missing_method" + + def test_tuple(self): + assert to_dict(("a", "b")) == ["a", "b"] + + def test_nested_list(self): + assert to_dict([Path("/a"), Path("/b")]) == ["/a", "/b"] + + def test_dict_values(self): + assert to_dict({"k": Path("/v")}) == {"k": "/v"} + + def test_primitive(self): + assert to_dict(42) == 42 + assert to_dict("hello") == "hello" + assert to_dict(None) is None + + +# --------------------------------------------------------------------------- +# Tool 1: derive_names +# --------------------------------------------------------------------------- + + +class TestDeriveNames: + def test_valid_package(self): + result = tool_derive_names("onesignal_flutter") + assert result["project_name"] == "flet-onesignal" + assert result["package_name"] == "flet_onesignal" + assert result["control_name"] == "Onesignal" + assert result["control_name_snake"] == "onesignal" + + def test_invalid_package(self): + with pytest.raises(ValueError, match="Invalid flutter_package"): + tool_derive_names("Invalid-Name") + + +# --------------------------------------------------------------------------- +# Tool 2: map_dart_type +# --------------------------------------------------------------------------- + + +class TestMapDartType: + def test_standard(self): + result = tool_map_dart_type("Future") + assert result["python_type"] == "str | None" + assert result["skipped"] is False + + def test_flet_aware(self): + result = tool_map_dart_type("Alignment", flet_aware=True) + assert result["python_type"] == "ft.Alignment" + assert result["skipped"] is False + + def test_flet_skipped(self): + result = tool_map_dart_type("Key", flet_aware=True) + assert result["python_type"] == "SKIPPED" + assert result["skipped"] is True + + +# --------------------------------------------------------------------------- +# Tool 5: scaffold +# --------------------------------------------------------------------------- + + +class TestScaffold: + def test_scaffold_service(self, tmp_path): + result = tool_scaffold( + template_name="service", + flutter_package="battery_plus", + project_name="flet-battery", + package_name="flet_battery", + control_name="Battery", + description="Test battery extension", + output_dir=str(tmp_path), + ) + project_dir = Path(result["project_dir"]) + assert project_dir.exists() + assert len(result["files"]) > 0 + # Check key files exist + assert (project_dir / "pyproject.toml").exists() + assert (project_dir / "src" / "flet_battery" / "__init__.py").exists() + + def test_scaffold_duplicate_raises(self, tmp_path): + kwargs = dict( + template_name="service", + flutter_package="battery_plus", + project_name="flet-battery", + package_name="flet_battery", + control_name="Battery", + output_dir=str(tmp_path), + ) + tool_scaffold(**kwargs) + with pytest.raises(FileExistsError): + tool_scaffold(**kwargs) + + +# --------------------------------------------------------------------------- +# Tool 6: run_pipeline (local fixture) +# --------------------------------------------------------------------------- + + +class TestRunPipeline: + def test_run_pipeline_local(self, tmp_path): + # First scaffold + tool_scaffold( + template_name="service", + flutter_package="onesignal_flutter", + project_name="flet-onesignal", + package_name="flet_onesignal", + control_name="OneSignal", + output_dir=str(tmp_path), + ) + + project_dir = tmp_path / "flet-onesignal" + result = tool_run_pipeline( + flutter_package="onesignal_flutter", + control_name="OneSignal", + extension_type="service", + project_dir=str(project_dir), + package_name="flet_onesignal", + control_name_snake="one_signal", + local_package_path=str(ONESIGNAL_FIXTURE), + ) + assert len(result["files_generated"]) > 0 + assert result["plan_summary"]["control_name"] == "OneSignal" + assert result["plan_summary"]["n_methods"] > 0 + + +# --------------------------------------------------------------------------- +# Resources +# --------------------------------------------------------------------------- + + +class TestResources: + def test_type_map_resource(self): + from flet_pkg.mcp.server import resource_type_map + + result = resource_type_map() + assert "String" in result["standard"] + assert result["standard"]["String"] == "str" + assert "Alignment" in result["flet_aware"] + assert "Key" in result["skipped"] + + def test_templates_resource(self): + from flet_pkg.mcp.server import resource_templates + + result = resource_templates() + assert "service" in result["templates"] + assert "ui_control" in result["templates"] + + +# --------------------------------------------------------------------------- +# Prompts +# --------------------------------------------------------------------------- + + +class TestPrompts: + def test_scaffold_service_prompt(self): + from flet_pkg.mcp.server import scaffold_service + + result = scaffold_service("battery_plus") + assert "battery_plus" in result + assert "service" in result + + def test_scaffold_ui_control_prompt(self): + from flet_pkg.mcp.server import scaffold_ui_control + + result = scaffold_ui_control("flutter_slidable") + assert "flutter_slidable" in result + assert "ui_control" in result + + def test_analyze_package_prompt(self): + from flet_pkg.mcp.server import analyze_package + + result = analyze_package("battery_plus", "service") + assert "battery_plus" in result + assert "service" in result + + +# --------------------------------------------------------------------------- +# Server registration +# --------------------------------------------------------------------------- + + +class TestServerRegistration: + def test_server_name(self): + assert mcp.name == "flet-pkg" + + def test_tools_registered(self): + tool_manager = mcp._tool_manager + tool_names = list(tool_manager._tools.keys()) + assert "tool_derive_names" in tool_names + assert "tool_map_dart_type" in tool_names + assert "tool_fetch_metadata" in tool_names + assert "tool_detect_extension_type" in tool_names + assert "tool_scaffold" in tool_names + assert "tool_run_pipeline" in tool_names + assert "tool_analyze_gaps" in tool_names diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..abe9a97 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,112 @@ +from flet_pkg.core.models import ( + DartClass, + DartEnum, + DartMethod, + DartPackageAPI, + DartParam, + EventPlan, + GenerationPlan, + MethodPlan, + ParamPlan, +) + + +class TestDartParam: + def test_defaults(self): + p = DartParam(name="foo") + assert p.name == "foo" + assert p.dart_type == "dynamic" + assert p.required is False + assert p.named is False + assert p.default is None + + def test_full(self): + p = DartParam(name="bar", dart_type="String", required=True, named=True, default='"hello"') + assert p.dart_type == "String" + assert p.required is True + assert p.default == '"hello"' + + +class TestDartMethod: + def test_defaults(self): + m = DartMethod(name="doSomething") + assert m.return_type == "void" + assert m.params == [] + assert m.is_async is False + + def test_with_params(self): + m = DartMethod( + name="login", + return_type="Future", + params=[DartParam(name="id", dart_type="String", required=True)], + is_async=True, + ) + assert m.is_async is True + assert len(m.params) == 1 + + +class TestDartEnum: + def test_basic(self): + e = DartEnum(name="LogLevel", values=[("debug", ""), ("info", ""), ("error", "")]) + assert len(e.values) == 3 + assert e.name == "LogLevel" + + +class TestDartClass: + def test_basic(self): + cls = DartClass( + name="MyService", + methods=[DartMethod(name="start")], + source_file="src/my_service.dart", + ) + assert cls.name == "MyService" + assert len(cls.methods) == 1 + assert cls.source_file == "src/my_service.dart" + + +class TestDartPackageAPI: + def test_empty(self): + api = DartPackageAPI() + assert api.classes == [] + assert api.enums == [] + + def test_with_data(self): + api = DartPackageAPI( + classes=[DartClass(name="Foo")], + enums=[DartEnum(name="Bar", values=[("a", ""), ("b", "")])], + ) + assert len(api.classes) == 1 + assert len(api.enums) == 1 + + +class TestParamPlan: + def test_auto_dart_name(self): + p = ParamPlan(python_name="user_id") + assert p.dart_name == "user_id" + + def test_explicit_dart_name(self): + p = ParamPlan(python_name="user_id", dart_name="userId") + assert p.dart_name == "userId" + + +class TestMethodPlan: + def test_auto_dart_method_name(self): + m = MethodPlan(python_name="do_thing") + assert m.dart_method_name == "do_thing" + + +class TestEventPlan: + def test_auto_dart_event_name(self): + e = EventPlan(python_attr_name="on_click", event_class_name="ClickEvent") + assert e.dart_event_name == "click" + + +class TestGenerationPlan: + def test_defaults(self): + plan = GenerationPlan(control_name="MyControl", package_name="my_pkg") + assert plan.base_class == "ft.Service" + assert plan.properties == [] + assert plan.main_methods == [] + assert plan.events == [] + assert plan.sub_modules == [] + assert plan.enums == [] diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py new file mode 100644 index 0000000..9c26438 --- /dev/null +++ b/tests/test_pipeline.py @@ -0,0 +1,282 @@ +from io import StringIO +from pathlib import Path +from unittest.mock import patch + +import pytest +from rich.console import Console + +from flet_pkg.core.ai.models import GapReport +from flet_pkg.core.pipeline import GenerationPipeline, PipelineResult +from flet_pkg.ui.coverage import print_coverage_score, print_coverage_table + +FIXTURE_PATH = Path(__file__).parent / "fixtures" / "onesignal_flutter" + + +@pytest.fixture +def pipeline(): + return GenerationPipeline() + + +def _setup_project(tmp_path): + project_dir = tmp_path / "flet-onesignal" + project_dir.mkdir() + (project_dir / "src" / "flet_onesignal").mkdir(parents=True) + (project_dir / "src" / "flutter" / "flet_onesignal" / "lib" / "src").mkdir(parents=True) + return project_dir + + +class TestPipelineWithFixtures: + """Integration tests using the test fixture Dart files.""" + + def test_full_pipeline_with_local_package(self, pipeline, tmp_path): + """Test the full pipeline using local test fixtures.""" + project_dir = _setup_project(tmp_path) + + result = pipeline.run( + flutter_package="onesignal_flutter", + control_name="OneSignal", + extension_type="service", + project_dir=project_dir, + package_name="flet_onesignal", + description="OneSignal integration", + local_package=FIXTURE_PATH, + ) + + assert isinstance(result, PipelineResult) + assert result.project_dir == project_dir + assert len(result.files_generated) > 0 + assert len(result.warnings) == 0 + + def test_generates_python_control(self, pipeline, tmp_path): + project_dir = _setup_project(tmp_path) + + pipeline.run( + flutter_package="onesignal_flutter", + control_name="OneSignal", + extension_type="service", + project_dir=project_dir, + package_name="flet_onesignal", + local_package=FIXTURE_PATH, + ) + + control_file = project_dir / "src" / "flet_onesignal" / "one_signal.py" + assert control_file.exists() + content = control_file.read_text() + assert "class OneSignal" in content + assert "ft.Service" in content + + def test_generates_dart_service(self, pipeline, tmp_path): + project_dir = _setup_project(tmp_path) + + pipeline.run( + flutter_package="onesignal_flutter", + control_name="OneSignal", + extension_type="service", + project_dir=project_dir, + package_name="flet_onesignal", + local_package=FIXTURE_PATH, + ) + + dart_file = ( + project_dir + / "src" + / "flutter" + / "flet_onesignal" + / "lib" + / "src" + / "one_signal_service.dart" + ) + assert dart_file.exists() + content = dart_file.read_text() + assert "OneSignalService" in content + assert "FletService" in content + + def test_generates_submodules(self, pipeline, tmp_path): + project_dir = _setup_project(tmp_path) + + pipeline.run( + flutter_package="onesignal_flutter", + control_name="OneSignal", + extension_type="service", + project_dir=project_dir, + package_name="flet_onesignal", + local_package=FIXTURE_PATH, + ) + + # Check that sub-module files were generated + python_dir = project_dir / "src" / "flet_onesignal" + py_files = list(python_dir.glob("*.py")) + py_names = [f.name for f in py_files] + # Should have at least: one_signal.py, __init__.py, types.py + assert "one_signal.py" in py_names + assert "__init__.py" in py_names + + +class TestPipelineErrors: + def test_download_failure_warns(self, pipeline, tmp_path): + project_dir = tmp_path / "project" + project_dir.mkdir() + + with patch.object( + pipeline.downloader, + "download", + side_effect=Exception("Network error"), + ): + result = pipeline.run( + flutter_package="bad_package", + control_name="Bad", + extension_type="service", + project_dir=project_dir, + package_name="flet_bad", + ) + + assert len(result.warnings) > 0 + assert "Download failed" in result.warnings[0] + assert result.files_generated == [] + + def test_empty_package_warns(self, pipeline, tmp_path): + project_dir = tmp_path / "project" + project_dir.mkdir() + + # Create empty lib directory + empty_pkg = tmp_path / "empty_flutter" + (empty_pkg / "lib").mkdir(parents=True) + + result = pipeline.run( + flutter_package="empty_flutter", + control_name="Empty", + extension_type="service", + project_dir=project_dir, + package_name="flet_empty", + local_package=empty_pkg, + ) + + assert len(result.warnings) > 0 + assert result.files_generated == [] + + +class TestPipelineCoverage: + """Tests for coverage score and gap report in pipeline results.""" + + def test_pipeline_result_has_coverage(self, pipeline, tmp_path): + """Coverage percentage and gap_report should be populated after a run.""" + project_dir = _setup_project(tmp_path) + + result = pipeline.run( + flutter_package="onesignal_flutter", + control_name="OneSignal", + extension_type="service", + project_dir=project_dir, + package_name="flet_onesignal", + local_package=FIXTURE_PATH, + ) + + assert result.coverage_pct > 0 + assert result.gap_report is not None + assert result.gap_report.total_dart_api > 0 + assert result.gap_report.total_generated > 0 + + def test_pipeline_verbose_runs_without_error(self, pipeline, tmp_path): + """Pipeline with verbose=True should complete without exceptions.""" + project_dir = _setup_project(tmp_path) + + result = pipeline.run( + flutter_package="onesignal_flutter", + control_name="OneSignal", + extension_type="service", + project_dir=project_dir, + package_name="flet_onesignal", + local_package=FIXTURE_PATH, + verbose=True, + ) + + assert len(result.files_generated) > 0 + assert result.coverage_pct > 0 + + def test_gap_report_has_category_counts(self, pipeline, tmp_path): + """Gap report should have per-category counts populated.""" + project_dir = _setup_project(tmp_path) + + result = pipeline.run( + flutter_package="onesignal_flutter", + control_name="OneSignal", + extension_type="service", + project_dir=project_dir, + package_name="flet_onesignal", + local_package=FIXTURE_PATH, + ) + + assert result.gap_report is not None + assert len(result.gap_report.category_counts) > 0 + # At least Methods should be present for a service package + assert "Methods" in result.gap_report.category_counts + + +class TestCoverageDisplay: + """Tests for the coverage UI functions.""" + + def test_print_coverage_score_format(self): + """Coverage score line should include percentage and fraction.""" + buf = StringIO() + test_console = Console(file=buf, force_terminal=False, width=80) + + with patch("flet_pkg.ui.coverage.console", test_console): + print_coverage_score(95.2, 40, 42) + + output = buf.getvalue() + assert "95.2%" in output + assert "40/42" in output + + def test_print_coverage_score_na_when_zero(self): + """Score should show N/A when there are no Dart APIs.""" + buf = StringIO() + test_console = Console(file=buf, force_terminal=False, width=80) + + with patch("flet_pkg.ui.coverage.console", test_console): + print_coverage_score(0.0, 0, 0) + + output = buf.getvalue() + assert "N/A" in output + + def test_print_coverage_table_renders(self): + """Coverage table should render categories and totals.""" + report = GapReport( + flutter_package="test_pkg", + extension_type="service", + total_dart_api=30, + total_generated=27, + coverage_pct=90.0, + category_counts={ + "Methods": (20, 18), + "Events": (5, 5), + "Enums": (5, 4), + }, + ) + + buf = StringIO() + test_console = Console(file=buf, force_terminal=False, width=100) + + with patch("flet_pkg.ui.coverage.console", test_console): + print_coverage_table(report) + + output = buf.getvalue() + assert "Methods" in output + assert "Events" in output + assert "Enums" in output + assert "Total" in output + + def test_print_coverage_table_empty_report(self): + """Table should not render when category_counts is empty.""" + report = GapReport( + flutter_package="test_pkg", + extension_type="service", + ) + + buf = StringIO() + test_console = Console(file=buf, force_terminal=False, width=80) + + with patch("flet_pkg.ui.coverage.console", test_console): + print_coverage_table(report) + + output = buf.getvalue() + assert output.strip() == "" diff --git a/tests/test_registry_checker.py b/tests/test_registry_checker.py new file mode 100644 index 0000000..563212b --- /dev/null +++ b/tests/test_registry_checker.py @@ -0,0 +1,217 @@ +from unittest.mock import MagicMock, patch + +import httpx + +from flet_pkg.core.registry_checker import ( + RegistryMatch, + check_flet_packages, + check_github, + check_pypi, +) + + +class TestCheckPyPI: + def test_found(self): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "info": { + "name": "flet-onesignal", + "project_url": "https://pypi.org/project/flet-onesignal/", + "summary": "Flet OneSignal extension", + } + } + + with patch("flet_pkg.core.registry_checker.httpx.get", return_value=mock_response): + result = check_pypi("flet-onesignal") + + assert result is not None + assert isinstance(result, RegistryMatch) + assert result.source == "PyPI" + assert result.name == "flet-onesignal" + assert result.url == "https://pypi.org/project/flet-onesignal/" + assert result.description == "Flet OneSignal extension" + + def test_not_found(self): + mock_response = MagicMock() + mock_response.status_code = 404 + + with patch("flet_pkg.core.registry_checker.httpx.get", return_value=mock_response): + result = check_pypi("nonexistent-pkg-xyz-12345") + + assert result is None + + def test_network_error(self): + with patch( + "flet_pkg.core.registry_checker.httpx.get", + side_effect=httpx.ConnectError("Connection refused"), + ): + result = check_pypi("flet-onesignal") + + assert result is None + + def test_timeout(self): + with patch( + "flet_pkg.core.registry_checker.httpx.get", + side_effect=httpx.TimeoutException("timed out"), + ): + result = check_pypi("flet-onesignal") + + assert result is None + + def test_server_error(self): + mock_response = MagicMock() + mock_response.status_code = 500 + + with patch("flet_pkg.core.registry_checker.httpx.get", return_value=mock_response): + result = check_pypi("flet-onesignal") + + assert result is None + + def test_fallback_url(self): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "info": { + "name": "my-pkg", + "summary": "", + } + } + + with patch("flet_pkg.core.registry_checker.httpx.get", return_value=mock_response): + result = check_pypi("my-pkg") + + assert result is not None + assert result.url == "https://pypi.org/project/my-pkg/" + + +class TestCheckGitHub: + def test_found(self): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "items": [ + { + "full_name": "user/flet-onesignal", + "html_url": "https://github.com/user/flet-onesignal", + "description": "Flet OneSignal extension", + }, + { + "full_name": "other/flet-onesignal-fork", + "html_url": "https://github.com/other/flet-onesignal-fork", + "description": None, + }, + ] + } + + with patch("flet_pkg.core.registry_checker.httpx.get", return_value=mock_response): + results = check_github("flet-onesignal") + + assert len(results) == 2 + assert results[0].source == "GitHub" + assert results[0].name == "user/flet-onesignal" + assert results[1].description == "" + + def test_no_results(self): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"items": []} + + with patch("flet_pkg.core.registry_checker.httpx.get", return_value=mock_response): + results = check_github("nonexistent-pkg-xyz") + + assert results == [] + + def test_network_error(self): + with patch( + "flet_pkg.core.registry_checker.httpx.get", + side_effect=httpx.ConnectError("Connection refused"), + ): + results = check_github("flet-onesignal") + + assert results == [] + + def test_max_results(self): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "items": [ + { + "full_name": f"user/flet-onesignal-{i}", + "html_url": f"https://github.com/user/flet-onesignal-{i}", + "description": f"Repo {i}", + } + for i in range(10) + ] + } + + with patch("flet_pkg.core.registry_checker.httpx.get", return_value=mock_response): + results = check_github("flet-onesignal", max_results=3) + + assert len(results) == 3 + + def test_rate_limited(self): + mock_response = MagicMock() + mock_response.status_code = 403 + + with patch("flet_pkg.core.registry_checker.httpx.get", return_value=mock_response): + results = check_github("flet-onesignal") + + assert results == [] + + +class TestCheckFletPackages: + def test_found(self): + mock_response = MagicMock() + mock_response.status_code = 200 + + with patch("flet_pkg.core.registry_checker.httpx.get", return_value=mock_response): + result = check_flet_packages("flet-rive") + + assert result is not None + assert result.source == "Flet SDK" + assert result.name == "flet-rive" + assert "flet-dev/flet" in result.url + assert "sdk/python/packages/flet-rive" in result.url + + def test_not_found(self): + mock_response = MagicMock() + mock_response.status_code = 404 + + with patch("flet_pkg.core.registry_checker.httpx.get", return_value=mock_response): + result = check_flet_packages("flet-nonexistent-xyz") + + assert result is None + + def test_network_error(self): + with patch( + "flet_pkg.core.registry_checker.httpx.get", + side_effect=httpx.ConnectError("Connection refused"), + ): + result = check_flet_packages("flet-rive") + + assert result is None + + def test_timeout(self): + with patch( + "flet_pkg.core.registry_checker.httpx.get", + side_effect=httpx.TimeoutException("timed out"), + ): + result = check_flet_packages("flet-rive") + + assert result is None + + +class TestRegistryMatch: + def test_defaults(self): + m = RegistryMatch(source="PyPI", name="pkg", url="https://example.com") + assert m.description == "" + + def test_with_description(self): + m = RegistryMatch( + source="GitHub", + name="user/repo", + url="https://github.com/user/repo", + description="A cool repo", + ) + assert m.description == "A cool repo" diff --git a/tests/test_scaffolder.py b/tests/test_scaffolder.py new file mode 100644 index 0000000..5b78569 --- /dev/null +++ b/tests/test_scaffolder.py @@ -0,0 +1,328 @@ +import pytest + +from flet_pkg.core.scaffolder import Scaffolder + + +@pytest.fixture +def service_context(): + return { + "project_name": "flet-test", + "package_name": "flet_test", + "control_name": "TestControl", + "control_name_snake": "test_control", + "flutter_package": "test_flutter", + "description": "A test extension", + "author": "Test Author", + "include_console": True, + } + + +class TestScaffolder: + def test_generates_project(self, tmp_path, service_context): + scaffolder = Scaffolder("service", service_context, tmp_path) + project_dir = scaffolder.generate() + + assert project_dir.exists() + assert project_dir.name == "flet-test" + + def test_generates_pyproject(self, tmp_path, service_context): + scaffolder = Scaffolder("service", service_context, tmp_path) + project_dir = scaffolder.generate() + + pyproject = project_dir / "pyproject.toml" + assert pyproject.exists() + content = pyproject.read_text() + assert 'name = "flet-test"' in content + + def test_generates_python_control(self, tmp_path, service_context): + scaffolder = Scaffolder("service", service_context, tmp_path) + project_dir = scaffolder.generate() + + control_file = project_dir / "src" / "flet_test" / "test_control.py" + assert control_file.exists() + content = control_file.read_text() + assert "class TestControl(ft.Service)" in content + assert '@ft.control("TestControl")' in content + + def test_generates_dart_extension(self, tmp_path, service_context): + scaffolder = Scaffolder("service", service_context, tmp_path) + project_dir = scaffolder.generate() + + ext_file = project_dir / "src" / "flutter" / "flet_test" / "lib" / "src" / "extension.dart" + assert ext_file.exists() + content = ext_file.read_text() + assert "createService" in content + assert '"TestControl"' in content + + def test_generates_dart_service(self, tmp_path, service_context): + scaffolder = Scaffolder("service", service_context, tmp_path) + project_dir = scaffolder.generate() + + svc_file = ( + project_dir + / "src" + / "flutter" + / "flet_test" + / "lib" + / "src" + / "test_control_service.dart" + ) + assert svc_file.exists() + content = svc_file.read_text() + assert "TestControlService" in content + + def test_ui_control_template(self, tmp_path): + context = { + "project_name": "flet-mywidget", + "package_name": "flet_mywidget", + "control_name": "MyWidget", + "control_name_snake": "my_widget", + "flutter_package": "my_widget_flutter", + "description": "A widget extension", + "author": "Test", + "include_console": True, + } + scaffolder = Scaffolder("ui_control", context, tmp_path) + project_dir = scaffolder.generate() + + control_file = project_dir / "src" / "flet_mywidget" / "my_widget.py" + assert control_file.exists() + content = control_file.read_text() + assert "ft.LayoutControl" in content + + ext_file = ( + project_dir / "src" / "flutter" / "flet_mywidget" / "lib" / "src" / "extension.dart" + ) + content = ext_file.read_text() + assert "createWidget" in content + + def test_raises_on_existing_dir(self, tmp_path, service_context): + (tmp_path / "flet-test").mkdir() + scaffolder = Scaffolder("service", service_context, tmp_path) + with pytest.raises(FileExistsError): + scaffolder.generate() + + def test_raises_on_invalid_template(self, tmp_path, service_context): + with pytest.raises(FileNotFoundError): + Scaffolder("nonexistent", service_context, tmp_path) + + def test_resolves_variable_in_paths(self, tmp_path, service_context): + scaffolder = Scaffolder("service", service_context, tmp_path) + project_dir = scaffolder.generate() + + # Check that variable-named directories are resolved + assert (project_dir / "src" / "flet_test").is_dir() + assert (project_dir / "src" / "flutter" / "flet_test").is_dir() + + def test_template_yaml_excluded(self, tmp_path, service_context): + scaffolder = Scaffolder("service", service_context, tmp_path) + project_dir = scaffolder.generate() + + # template.yaml should not appear in output + for p in project_dir.rglob("template.yaml"): + pytest.fail(f"template.yaml found in output at {p}") + + def test_service_stub_has_on_error(self, tmp_path, service_context): + scaffolder = Scaffolder("service", service_context, tmp_path) + project_dir = scaffolder.generate() + + # Control file has on_error handler + control_file = project_dir / "src" / "flet_test" / "test_control.py" + content = control_file.read_text() + assert "on_error" in content + assert "TestControlErrorEvent" in content + + # types.py has ErrorEvent dataclass + types_file = project_dir / "src" / "flet_test" / "types.py" + assert types_file.exists() + types_content = types_file.read_text() + assert "class TestControlErrorEvent" in types_content + assert "ft.Event" in types_content + + # __init__.py exports ErrorEvent + init_file = project_dir / "src" / "flet_test" / "__init__.py" + init_content = init_file.read_text() + assert "TestControlErrorEvent" in init_content + + # Dart service has _handleError + svc_file = ( + project_dir + / "src" + / "flutter" + / "flet_test" + / "lib" + / "src" + / "test_control_service.dart" + ) + dart_content = svc_file.read_text() + assert "_handleError" in dart_content + assert "triggerEvent" in dart_content + + # Example has on_error + example_file = project_dir / "examples" / "flet_test_example" / "src" / "main.py" + example_content = example_file.read_text() + assert "on_error" in example_content + + def test_generates_about_pkg_flet_guide(self, tmp_path, service_context): + scaffolder = Scaffolder("service", service_context, tmp_path) + project_dir = scaffolder.generate() + + guide = project_dir / "about_pkg_flet.md" + assert guide.exists() + content = guide.read_text() + + # Context variables are rendered + assert "flet-test" in content + assert "flet_test" in content + assert "TestControl" in content + assert "test_control" in content + assert "test_flutter" in content + + # All 4 part headers present + assert "Part 1" in content + assert "Part 2" in content + assert "Part 3" in content + assert "Part 4" in content + + # Skill-level labels + assert "Beginner" in content + assert "Intermediate" in content + assert "Advanced" in content + assert "Practical" in content + + # Branding header + assert "About Package for Flet" in content + assert "flet-pkg" in content + + # Analogy + assert "remote control" in content + + # Debugging checklist + assert "Debugging Checklist" in content + assert "page.update()" in content + + # LogCat section + assert "Android LogCat" in content + assert "flet_log.py" in content + assert "flet_log.sh" in content + + # Scripts directory exists with both files + assert (project_dir / "scripts" / "flet_log.py").exists() + assert (project_dir / "scripts" / "flet_log.sh").exists() + + # Service-specific content + assert "ft.Service" in content + assert "createService" in content + + # Wheel / uv build section + assert "uv build" in content + assert "py3-none-any.whl" in content + + # No unrendered Jinja2 variables remain + assert "{{" not in content + assert "}}" not in content + + def test_ui_control_generates_about_pkg_flet_guide(self, tmp_path): + context = { + "project_name": "flet-mywidget", + "package_name": "flet_mywidget", + "control_name": "MyWidget", + "control_name_snake": "my_widget", + "flutter_package": "my_widget_flutter", + "description": "A widget extension", + "author": "Test", + "include_console": True, + } + scaffolder = Scaffolder("ui_control", context, tmp_path) + project_dir = scaffolder.generate() + + guide = project_dir / "about_pkg_flet.md" + assert guide.exists() + content = guide.read_text() + + # Context variables are rendered + assert "flet-mywidget" in content + assert "flet_mywidget" in content + assert "MyWidget" in content + assert "my_widget" in content + assert "my_widget_flutter" in content + + # All 4 part headers present + assert "Part 1" in content + assert "Part 2" in content + assert "Part 3" in content + assert "Part 4" in content + + # Skill-level labels + assert "Beginner" in content + assert "Intermediate" in content + assert "Advanced" in content + assert "Practical" in content + + # Branding header + assert "About Package for Flet" in content + assert "flet-pkg" in content + + # Analogy + assert "remote control" in content + + # Debugging checklist + assert "Debugging Checklist" in content + assert "page.update()" in content + + # LogCat section + assert "Android LogCat" in content + assert "flet_log.py" in content + assert "flet_log.sh" in content + + # Scripts directory exists with both files + assert (project_dir / "scripts" / "flet_log.py").exists() + assert (project_dir / "scripts" / "flet_log.sh").exists() + + # UI Control-specific content + assert "ft.LayoutControl" in content + assert "createWidget" in content + + # Wheel / uv build section + assert "uv build" in content + assert "py3-none-any.whl" in content + + # No unrendered Jinja2 variables remain + assert "{{" not in content + assert "}}" not in content + + def test_ui_control_stub_has_on_error(self, tmp_path): + context = { + "project_name": "flet-mywidget", + "package_name": "flet_mywidget", + "control_name": "MyWidget", + "control_name_snake": "my_widget", + "flutter_package": "my_widget_flutter", + "description": "A widget extension", + "author": "Test", + "include_console": True, + } + scaffolder = Scaffolder("ui_control", context, tmp_path) + project_dir = scaffolder.generate() + + # Control file has on_error handler + control_file = project_dir / "src" / "flet_mywidget" / "my_widget.py" + content = control_file.read_text() + assert "on_error" in content + assert "MyWidgetErrorEvent" in content + + # types.py has ErrorEvent dataclass + types_file = project_dir / "src" / "flet_mywidget" / "types.py" + assert types_file.exists() + types_content = types_file.read_text() + assert "class MyWidgetErrorEvent" in types_content + + # __init__.py exports ErrorEvent + init_file = project_dir / "src" / "flet_mywidget" / "__init__.py" + init_content = init_file.read_text() + assert "MyWidgetErrorEvent" in init_content + + # Example has on_error + example_file = project_dir / "examples" / "flet_mywidget_example" / "src" / "main.py" + example_content = example_file.read_text() + assert "on_error" in example_content diff --git a/tests/test_service_coverage.py b/tests/test_service_coverage.py new file mode 100644 index 0000000..5554983 --- /dev/null +++ b/tests/test_service_coverage.py @@ -0,0 +1,317 @@ +""" +Comprehensive coverage tests for Service (non-visual) packages. + +Downloads the 10 most popular Flutter service packages from pub.dev, +runs the full parse → analyze pipeline, and measures how much of the +Dart package API was successfully mapped to Python/Flet code. + +Coverage dimensions: + 1. Method coverage — Dart class methods mapped to Python async methods + 2. Enum coverage — Dart enums (including re-exported) mapped to Python enums + 3. Event coverage — Stream getters mapped to event handlers + 4. Data class coverage — re-exported types resolved to stub dataclasses + +These tests require network access on first run; subsequent runs use +the cache at ~/.cache/flet-pkg/. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +import pytest + +from flet_pkg.core.analyzer import PackageAnalyzer +from flet_pkg.core.downloader import PubDevDownloader +from flet_pkg.core.models import DartPackageAPI, GenerationPlan +from flet_pkg.core.parser import parse_dart_package_api + +# -- Top 10 Flutter Service packages ----------------------------------------- + +SERVICE_PACKAGES = [ + { + "flutter_package": "shared_preferences", + "control_name": "SharedPreferences", + "description": "Persistent key-value storage", + }, + { + "flutter_package": "path_provider", + "control_name": "PathProvider", + "description": "File system path provider", + }, + { + "flutter_package": "connectivity_plus", + "control_name": "Connectivity", + "description": "Network connectivity detection", + }, + { + "flutter_package": "device_info_plus", + "control_name": "DeviceInfo", + "description": "Device hardware and OS information", + }, + { + "flutter_package": "package_info_plus", + "control_name": "PackageInfo", + "description": "App package metadata", + }, + { + "flutter_package": "permission_handler", + "control_name": "PermissionHandler", + "description": "Runtime permission management", + }, + { + "flutter_package": "geolocator", + "control_name": "Geolocator", + "description": "GPS and location services", + }, + { + "flutter_package": "local_auth", + "control_name": "LocalAuth", + "description": "Biometric and local authentication", + }, + { + "flutter_package": "image_picker", + "control_name": "ImagePicker", + "description": "Camera and gallery image selection", + }, + { + "flutter_package": "file_picker", + "control_name": "FilePicker", + "description": "Cross-platform file selection", + }, +] + +MIN_COVERAGE_PCT = 70.0 + + +# -- Coverage report dataclass ----------------------------------------------- + + +@dataclass +class ServiceCoverageReport: + """Full coverage metrics for a single Flutter service package.""" + + flutter_package: str + control_name: str + + # Source API counts (after filtering internal/mock methods) + total_dart_methods: int = 0 + total_dart_stream_getters: int = 0 + total_dart_enums: int = 0 + total_dart_helpers: int = 0 + total_reexported_types: int = 0 + + # Generated plan counts + generated_methods: int = 0 + generated_sub_module_methods: int = 0 + generated_events: int = 0 + generated_enums: int = 0 + generated_stubs: int = 0 + generated_sub_modules: int = 0 + + @property + def total_generated_methods(self) -> int: + return self.generated_methods + self.generated_sub_module_methods + + @property + def total_source_api(self) -> int: + """Total source API items (methods + stream getters).""" + return self.total_dart_methods + self.total_dart_stream_getters + + @property + def total_generated_api(self) -> int: + """Total generated API items (methods + events from streams).""" + return self.total_generated_methods + self.generated_events + + @property + def method_coverage(self) -> float: + """Dart API items mapped to Python (methods + stream→events).""" + if self.total_source_api == 0: + return 100.0 + return self.total_generated_api / self.total_source_api * 100 + + @property + def enum_coverage(self) -> float: + """Dart enums (including re-exported) mapped to Python enums/stubs.""" + total = self.total_dart_enums + self.total_reexported_types + if total == 0: + return 100.0 + generated = self.generated_enums + self.generated_stubs + return generated / total * 100 + + @property + def overall_feature_coverage(self) -> float: + """Weighted overall: methods 70%, enums+types 30%.""" + return self.method_coverage * 0.7 + self.enum_coverage * 0.3 + + def summary(self) -> str: + lines = [ + f" Package: {self.flutter_package}", + f" Control: {self.control_name}", + "", + f" API items: {self.total_generated_api}/{self.total_source_api}" + f" ({self.method_coverage:.0f}%)", + f" -> methods: {self.total_generated_methods}/{self.total_dart_methods}", + f" -> events: {self.generated_events}/{self.total_dart_stream_getters}", + f" -> sub-modules: {self.generated_sub_modules}", + f" Enums: {self.generated_enums}/{self.total_dart_enums}", + f" Stubs: {self.generated_stubs}/{self.total_reexported_types}", + f" Helpers: {self.total_dart_helpers}", + "", + f" OVERALL: {self.overall_feature_coverage:.1f}%", + ] + return "\n".join(lines) + + +# -- Helpers ------------------------------------------------------------------ + + +# Internal/mock methods excluded from coverage (not real user API) +_INTERNAL_METHOD_NAMES = {"setMockInitialValues", "setMockMethodCallHandler"} + + +def _build_service_report( + flutter_package: str, + control_name: str, + api: DartPackageAPI, + plan: GenerationPlan, +) -> ServiceCoverageReport: + """Build a coverage report for a service package.""" + report = ServiceCoverageReport( + flutter_package=flutter_package, + control_name=control_name, + ) + + # Source API — separate regular methods from stream getters + for cls in api.classes: + for m in cls.methods: + if m.name in _INTERNAL_METHOD_NAMES: + continue + if m.is_getter and m.return_type.startswith("Stream"): + report.total_dart_stream_getters += 1 + else: + report.total_dart_methods += 1 + # Include top-level functions as methods (path_provider uses these) + report.total_dart_methods += len(api.top_level_functions) + report.total_dart_enums = len(api.enums) + report.total_dart_helpers = len(api.helper_classes) + report.total_reexported_types = len(api.reexported_types) + + # Plan + report.generated_methods = len(plan.main_methods) + report.generated_sub_module_methods = sum(len(s.methods) for s in plan.sub_modules) + report.generated_events = len(plan.events) + report.generated_enums = len(plan.enums) + report.generated_stubs = len(plan.stub_data_classes) + report.generated_sub_modules = len(plan.sub_modules) + + return report + + +# -- Fixtures ----------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def downloader(): + return PubDevDownloader() + + +@pytest.fixture(scope="module") +def analyzer(): + return PackageAnalyzer() + + +# -- Tests -------------------------------------------------------------------- + + +@pytest.mark.network +@pytest.mark.parametrize( + "pkg", + SERVICE_PACKAGES, + ids=[p["flutter_package"] for p in SERVICE_PACKAGES], +) +class TestServiceCoverage: + def _run_analysis(self, pkg, downloader, analyzer): + """Download, parse, and analyze a Flutter service package.""" + flutter_package = pkg["flutter_package"] + control_name = pkg["control_name"] + package_name = f"flet_{flutter_package.removesuffix('_plus')}" + + package_path = downloader.download(flutter_package) + api = parse_dart_package_api(package_path) + plan = analyzer.analyze( + api, + control_name=control_name, + extension_type="service", + flutter_package=flutter_package, + package_name=package_name, + description=pkg["description"], + ) + return api, plan + + def test_method_coverage(self, pkg, downloader, analyzer): + """Method coverage: Dart methods mapped to Python async methods.""" + api, plan = self._run_analysis(pkg, downloader, analyzer) + report = _build_service_report(pkg["flutter_package"], pkg["control_name"], api, plan) + + print(f"\n{'=' * 60}") + print(report.summary()) + print(f"{'=' * 60}") + + assert report.method_coverage >= MIN_COVERAGE_PCT, ( + f"{pkg['flutter_package']}: method coverage " + f"{report.method_coverage:.1f}% < {MIN_COVERAGE_PCT}%" + ) + + def test_produces_output(self, pkg, downloader, analyzer): + """Pipeline must produce at least some methods or events.""" + api, plan = self._run_analysis(pkg, downloader, analyzer) + + total = ( + len(plan.main_methods) + + sum(len(s.methods) for s in plan.sub_modules) + + len(plan.events) + + len(plan.enums) + ) + assert total > 0, f"{pkg['flutter_package']}: nothing generated" + + def test_overall_coverage(self, pkg, downloader, analyzer): + """Weighted overall coverage must be >= 70%.""" + api, plan = self._run_analysis(pkg, downloader, analyzer) + report = _build_service_report(pkg["flutter_package"], pkg["control_name"], api, plan) + + assert report.overall_feature_coverage >= MIN_COVERAGE_PCT, ( + f"{pkg['flutter_package']}: overall coverage " + f"{report.overall_feature_coverage:.1f}% < {MIN_COVERAGE_PCT}%" + ) + + def test_enum_coverage(self, pkg, downloader, analyzer): + """Direct Dart enums should be mapped. + + Note: re-exported types from platform_interface packages are only + fully resolved when the pipeline runs resolve_platform_types(). + This test checks parse+analyze coverage, so we only assert on + direct enums. Re-exported type resolution is logged but not asserted. + """ + api, plan = self._run_analysis(pkg, downloader, analyzer) + report = _build_service_report(pkg["flutter_package"], pkg["control_name"], api, plan) + + # Direct enums (from the package itself) + if report.total_dart_enums == 0 and report.total_reexported_types == 0: + pytest.skip("No enums or re-exported types in this package") + + if report.total_dart_enums > 0: + direct_pct = report.generated_enums / report.total_dart_enums * 100 + assert direct_pct >= MIN_COVERAGE_PCT, ( + f"{pkg['flutter_package']}: direct enum coverage " + f"{direct_pct:.1f}% < {MIN_COVERAGE_PCT}% " + f"({report.generated_enums}/{report.total_dart_enums})" + ) + + # Log re-exported type resolution (informational, not asserted) + if report.total_reexported_types > 0: + resolved = report.generated_enums + report.generated_stubs + print( + f" Re-exported types: {resolved}/{report.total_reexported_types} " + f"resolved (full pipeline needed for complete resolution)" + ) diff --git a/tests/test_type_map.py b/tests/test_type_map.py new file mode 100644 index 0000000..46dc438 --- /dev/null +++ b/tests/test_type_map.py @@ -0,0 +1,227 @@ +from flet_pkg.core.type_map import ( + get_flet_dart_getter, + map_dart_type, + map_dart_type_flet, + map_return_type, +) + + +class TestMapDartType: + def test_string(self): + assert map_dart_type("String") == "str" + + def test_bool(self): + assert map_dart_type("bool") == "bool" + + def test_int(self): + assert map_dart_type("int") == "int" + + def test_double(self): + assert map_dart_type("double") == "float" + + def test_num(self): + assert map_dart_type("num") == "float" + + def test_void(self): + assert map_dart_type("void") == "None" + + def test_dynamic(self): + assert map_dart_type("dynamic") == "Any" + + def test_object(self): + assert map_dart_type("Object") == "Any" + + def test_color(self): + assert map_dart_type("Color") == "str" + + def test_nullable_string(self): + assert map_dart_type("String?") == "str | None" + + def test_nullable_bool(self): + assert map_dart_type("bool?") == "bool | None" + + def test_nullable_int(self): + assert map_dart_type("int?") == "int | None" + + def test_list_string(self): + assert map_dart_type("List") == "list[str]" + + def test_list_int(self): + assert map_dart_type("List") == "list[int]" + + def test_map_string_dynamic(self): + assert map_dart_type("Map") == "dict[str, Any]" + + def test_map_string_string(self): + assert map_dart_type("Map") == "dict[str, str]" + + def test_set(self): + assert map_dart_type("Set") == "set[str]" + + def test_future_void(self): + assert map_dart_type("Future") == "None" + + def test_future_string(self): + assert map_dart_type("Future") == "str" + + def test_future_bool(self): + assert map_dart_type("Future") == "bool" + + def test_nested_generic(self): + assert map_dart_type("Map>") == "dict[str, list[int]]" + + def test_empty(self): + assert map_dart_type("") == "Any" + + def test_unknown_type(self): + assert map_dart_type("CustomWidget") == "CustomWidget" + + def test_function_type(self): + assert map_dart_type("Function") == "Any" + + def test_iterable(self): + assert map_dart_type("Iterable") == "list[str]" + + +class TestMapDartTypeFlet: + def test_alignment(self): + assert map_dart_type_flet("Alignment") == "ft.Alignment" + + def test_alignment_geometry(self): + assert map_dart_type_flet("AlignmentGeometry") == "ft.Alignment" + + def test_nullable_alignment(self): + assert map_dart_type_flet("Alignment?") == "ft.Alignment | None" + + def test_box_fit(self): + assert map_dart_type_flet("BoxFit") == "ft.BoxFit" + + def test_rect(self): + assert map_dart_type_flet("Rect") == "ft.Rect" + + def test_color(self): + assert map_dart_type_flet("Color") == "ft.Color" + + def test_double(self): + assert map_dart_type_flet("double") == "ft.Number" + + def test_num(self): + assert map_dart_type_flet("num") == "ft.Number" + + def test_widget(self): + assert map_dart_type_flet("Widget") == "ft.Control" + + def test_key_returns_none(self): + assert map_dart_type_flet("Key") is None + + def test_nullable_key_returns_none(self): + assert map_dart_type_flet("Key?") is None + + def test_string_fallback(self): + assert map_dart_type_flet("String") == "str" + + def test_bool_fallback(self): + assert map_dart_type_flet("bool") == "bool" + + def test_int_fallback(self): + assert map_dart_type_flet("int") == "int" + + def test_list_widget(self): + assert map_dart_type_flet("List") == "list[ft.Control]" + + def test_list_string(self): + assert map_dart_type_flet("List") == "list[str]" + + def test_future_alignment(self): + assert map_dart_type_flet("Future") == "ft.Alignment" + + def test_unknown_type_passthrough(self): + assert map_dart_type_flet("CustomThing") == "CustomThing" + + def test_fit_maps_to_box_fit(self): + assert map_dart_type_flet("Fit") == "ft.BoxFit" + + def test_nullable_fit(self): + assert map_dart_type_flet("Fit?") == "ft.BoxFit | None" + + def test_empty(self): + assert map_dart_type_flet("") == "Any" + + +class TestGetFletDartGetter: + def test_alignment(self): + result = get_flet_dart_getter("ft.Alignment", "alignment") + assert result == 'control.getAlignment("alignment")' + + def test_box_fit(self): + result = get_flet_dart_getter("ft.BoxFit", "fit") + assert result == 'control.getBoxFit("fit")' + + def test_rect(self): + result = get_flet_dart_getter("ft.Rect", "rect") + assert result == 'control.getRect("rect")' + + def test_number(self): + result = get_flet_dart_getter("ft.Number", "size") + assert result == 'control.getDouble("size")' + + def test_control(self): + result = get_flet_dart_getter("ft.Control", "child") + assert result == 'buildWidget("child")' + + def test_bool(self): + result = get_flet_dart_getter("bool", "enabled") + assert result == 'control.getBool("enabled", false)!' + + def test_str(self): + result = get_flet_dart_getter("str", "name") + assert result == 'control.getString("name")' + + def test_nullable_type_strips_none(self): + result = get_flet_dart_getter("ft.Alignment | None", "alignment") + assert result == 'control.getAlignment("alignment")' + + def test_list_control(self): + result = get_flet_dart_getter("list[ft.Control]", "children") + assert result == 'buildWidgets("children")' + + def test_unknown_fallback(self): + result = get_flet_dart_getter("SomeCustomType", "custom") + assert result == 'control.getString("custom")' + + +class TestMapReturnType: + def test_void(self): + py_type, is_async = map_return_type("void") + assert py_type == "None" + assert is_async is False + + def test_future_void(self): + py_type, is_async = map_return_type("Future") + assert py_type == "None" + assert is_async is True + + def test_future_string(self): + py_type, is_async = map_return_type("Future") + assert py_type == "str" + assert is_async is True + + def test_future_bool(self): + py_type, is_async = map_return_type("Future") + assert py_type == "bool" + assert is_async is True + + def test_bare_future(self): + py_type, is_async = map_return_type("Future") + assert py_type == "None" + assert is_async is True + + def test_string(self): + py_type, is_async = map_return_type("String") + assert py_type == "str" + assert is_async is False + + def test_empty(self): + py_type, is_async = map_return_type("") + assert py_type == "None" + assert is_async is False diff --git a/tests/test_ui_control_coverage.py b/tests/test_ui_control_coverage.py new file mode 100644 index 0000000..6b2d564 --- /dev/null +++ b/tests/test_ui_control_coverage.py @@ -0,0 +1,366 @@ +""" +Comprehensive coverage tests for UI Control (visual widget) packages. + +Downloads the 10 most popular Flutter visual widget packages from pub.dev, +runs the full parse → analyze pipeline, and measures how much of the +Dart package API was successfully mapped to Python/Flet code. + +Coverage dimensions: + 1. Widget class coverage — how many widget classes were processed + 2. Property coverage — constructor params mapped to properties/events + 3. Enum coverage — Dart enums mapped to Python enums + +These tests require network access on first run; subsequent runs use +the cache at ~/.cache/flet-pkg/. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +import pytest + +from flet_pkg.core.analyzer import PackageAnalyzer +from flet_pkg.core.downloader import PubDevDownloader +from flet_pkg.core.models import DartClass, DartPackageAPI, GenerationPlan +from flet_pkg.core.parser import parse_dart_package_api + +# -- Top 10 Flutter UI Control packages ------------------------------------ + +UI_CONTROL_PACKAGES = [ + { + "flutter_package": "flutter_spinkit", + "control_name": "SpinKit", + "description": "Loading indicators animated with Flutter", + }, + { + "flutter_package": "shimmer", + "control_name": "Shimmer", + "description": "Shimmer loading effect widget", + }, + { + "flutter_package": "carousel_slider", + "control_name": "CarouselSlider", + "description": "Carousel slider widget", + }, + { + "flutter_package": "percent_indicator", + "control_name": "PercentIndicator", + "description": "Circular and linear percent indicators", + }, + { + "flutter_package": "flutter_rating_bar", + "control_name": "RatingBar", + "description": "Customizable rating bar widget", + }, + { + "flutter_package": "flutter_slidable", + "control_name": "Slidable", + "description": "Slidable list item widget", + }, + { + "flutter_package": "smooth_page_indicator", + "control_name": "SmoothPageIndicator", + "description": "Smooth animated page indicators", + }, + { + "flutter_package": "readmore", + "control_name": "ReadMore", + "description": "Expandable text widget", + }, + { + "flutter_package": "marquee", + "control_name": "Marquee", + "description": "Scrolling text marquee widget", + }, + { + "flutter_package": "auto_size_text", + "control_name": "AutoSizeText", + "description": "Auto-resizing text widget", + }, +] + +MIN_COVERAGE_PCT = 70.0 + +# Params always excluded from coverage counting (framework internals) +_FRAMEWORK_PARAMS = {"key", "child", "children"} + + +# -- Coverage report dataclass --------------------------------------------- + + +@dataclass +class CoverageReport: + """Full coverage metrics for a single Flutter package.""" + + flutter_package: str + main_widget: str + + # Widget classes + total_widget_classes: int = 0 + processed_widget_classes: int = 0 # main + sub-controls + + # Constructor params (across ALL widget classes in the package) + total_params_all_widgets: int = 0 + # Constructor params (main widget only) + total_params_main: int = 0 + generated_properties: int = 0 + generated_events: int = 0 + sub_control_features: int = 0 + + # Enums + total_dart_enums: int = 0 + generated_enums: int = 0 + + # Data classes + total_helper_classes: int = 0 + generated_data_classes: int = 0 + + @property + def main_widget_generated(self) -> int: + return self.generated_properties + self.generated_events + self.sub_control_features + + @property + def main_widget_coverage(self) -> float: + """Coverage of main widget params (properties + events).""" + if self.total_params_main == 0: + return 100.0 + return self.main_widget_generated / self.total_params_main * 100 + + @property + def widget_class_coverage(self) -> float: + """How many widget classes were processed.""" + if self.total_widget_classes == 0: + return 100.0 + return self.processed_widget_classes / self.total_widget_classes * 100 + + @property + def enum_coverage(self) -> float: + """Dart enums mapped to Python enums.""" + if self.total_dart_enums == 0: + return 100.0 + return self.generated_enums / self.total_dart_enums * 100 + + @property + def overall_feature_coverage(self) -> float: + """Weighted overall coverage across all dimensions. + + Weights: params 60%, enums 20%, widget classes 20%. + """ + w_params = 0.6 + w_enums = 0.2 + w_classes = 0.2 + return ( + self.main_widget_coverage * w_params + + self.enum_coverage * w_enums + + self.widget_class_coverage * w_classes + ) + + def summary(self) -> str: + lines = [ + f" Package: {self.flutter_package}", + f" Main widget: {self.main_widget}", + "", + f" Widget classes: {self.processed_widget_classes}" + f"/{self.total_widget_classes}" + f" ({self.widget_class_coverage:.0f}%)", + f" Main widget params: {self.total_params_main}", + f" -> properties: {self.generated_properties}", + f" -> events: {self.generated_events}", + f" -> sub-ctrl: {self.sub_control_features}", + f" -> COVERAGE: {self.main_widget_coverage:.1f}%", + f" All widgets params: {self.total_params_all_widgets}", + f" Enums: {self.generated_enums}/{self.total_dart_enums} ({self.enum_coverage:.0f}%)", + f" Data classes: {self.generated_data_classes}/{self.total_helper_classes}", + "", + f" OVERALL: {self.overall_feature_coverage:.1f}%", + ] + return "\n".join(lines) + + +# -- Helpers ---------------------------------------------------------------- + + +def _count_eligible_params(widget: DartClass) -> int: + """Count constructor params excluding framework internals.""" + return sum(1 for p in widget.constructor_params if p.name not in _FRAMEWORK_PARAMS) + + +def _count_sub_control_features(sub_controls) -> int: + """Recursively count properties + events in sub-controls.""" + total = 0 + for sc in sub_controls: + total += len(sc.properties) + len(sc.events) + total += _count_sub_control_features(sc.sub_controls) + return total + + +def _count_sub_control_classes(sub_controls) -> int: + """Recursively count sub-control classes (each is a processed widget).""" + total = 0 + for sc in sub_controls: + total += 1 + total += _count_sub_control_classes(sc.sub_controls) + return total + + +def _build_coverage_report( + flutter_package: str, + api: DartPackageAPI, + plan: GenerationPlan, +) -> CoverageReport: + """Build a comprehensive coverage report from parsed API + plan.""" + widget_classes = [c for c in api.classes if c.constructor_params] + + # Find the main widget + main_widget = next( + (c for c in widget_classes if c.name == plan.dart_main_class), + widget_classes[0] if widget_classes else None, + ) + + report = CoverageReport( + flutter_package=flutter_package, + main_widget=plan.dart_main_class or "?", + ) + + # Widget class counts + report.total_widget_classes = len(widget_classes) + n_sub = _count_sub_control_classes(plan.sub_controls) + + # Family: all widget classes are covered via the enum type + if plan.widget_family_variants: + report.processed_widget_classes = len(plan.widget_family_variants) + # Siblings: main + each sibling + elif plan.sibling_widgets: + report.processed_widget_classes = 1 + len(plan.sibling_widgets) + n_sub + else: + report.processed_widget_classes = 1 + n_sub # main + sub-controls + + # Params across ALL widget classes + report.total_params_all_widgets = sum(_count_eligible_params(w) for w in widget_classes) + + # Main widget params + # For families, count shared params (intersection) rather than + # the template's total, since only shared params are generated. + if plan.widget_family_variants and len(widget_classes) > 1: + _fw = {"key", "child", "children"} + param_sets = [ + {p.name for p in w.constructor_params if p.name not in _fw} for w in widget_classes + ] + shared = param_sets[0].copy() + for s in param_sets[1:]: + shared &= s + # +1 for the "type" property that the family processor adds + report.total_params_main = len(shared) + 1 + elif main_widget: + report.total_params_main = _count_eligible_params(main_widget) + + # Generated features + report.generated_properties = len(plan.properties) + report.generated_events = len(plan.events) + report.sub_control_features = _count_sub_control_features( + plan.sub_controls, + ) + + # Enums + report.total_dart_enums = len(api.enums) + report.generated_enums = len(plan.enums) + + # Helper/data classes + report.total_helper_classes = len(api.helper_classes) + report.generated_data_classes = len(plan.stub_data_classes) + + return report + + +# -- Fixtures --------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def downloader(): + return PubDevDownloader() + + +@pytest.fixture(scope="module") +def analyzer(): + return PackageAnalyzer() + + +# -- Tests ------------------------------------------------------------------ + + +@pytest.mark.network +@pytest.mark.parametrize( + "pkg", + UI_CONTROL_PACKAGES, + ids=[p["flutter_package"] for p in UI_CONTROL_PACKAGES], +) +class TestUIControlCoverage: + def _run_analysis(self, pkg, downloader, analyzer): + """Download, parse, and analyze a Flutter package.""" + flutter_package = pkg["flutter_package"] + control_name = pkg["control_name"] + package_name = f"flet_{flutter_package.removeprefix('flutter_')}" + + package_path = downloader.download(flutter_package) + api = parse_dart_package_api(package_path, include_widgets=True) + plan = analyzer.analyze( + api, + control_name=control_name, + extension_type="ui_control", + flutter_package=flutter_package, + package_name=package_name, + description=pkg["description"], + ) + return api, plan + + def test_main_widget_coverage(self, pkg, downloader, analyzer): + """Main widget property/event coverage must be >= 70%.""" + api, plan = self._run_analysis(pkg, downloader, analyzer) + report = _build_coverage_report(pkg["flutter_package"], api, plan) + + print(f"\n{'=' * 60}") + print(report.summary()) + print(f"{'=' * 60}") + + assert report.main_widget_coverage >= MIN_COVERAGE_PCT, ( + f"{pkg['flutter_package']}: main widget coverage " + f"{report.main_widget_coverage:.1f}% < {MIN_COVERAGE_PCT}%" + ) + + def test_enum_coverage(self, pkg, downloader, analyzer): + """All Dart enums in the package should be mapped.""" + api, plan = self._run_analysis(pkg, downloader, analyzer) + report = _build_coverage_report(pkg["flutter_package"], api, plan) + + if report.total_dart_enums == 0: + pytest.skip("No enums in this package") + + assert report.enum_coverage >= MIN_COVERAGE_PCT, ( + f"{pkg['flutter_package']}: enum coverage " + f"{report.enum_coverage:.1f}% < {MIN_COVERAGE_PCT}% " + f"({report.generated_enums}/{report.total_dart_enums})" + ) + + def test_overall_coverage(self, pkg, downloader, analyzer): + """Weighted overall coverage must be >= 70%.""" + api, plan = self._run_analysis(pkg, downloader, analyzer) + report = _build_coverage_report(pkg["flutter_package"], api, plan) + + assert report.overall_feature_coverage >= MIN_COVERAGE_PCT, ( + f"{pkg['flutter_package']}: overall coverage " + f"{report.overall_feature_coverage:.1f}% < {MIN_COVERAGE_PCT}%" + ) + + def test_produces_output(self, pkg, downloader, analyzer): + """Pipeline must produce at least some properties or events.""" + api, plan = self._run_analysis(pkg, downloader, analyzer) + + total = ( + len(plan.properties) + + len(plan.events) + + len(plan.enums) + + _count_sub_control_features(plan.sub_controls) + ) + assert total > 0, f"{pkg['flutter_package']}: nothing generated" + assert plan.dart_main_class, f"{pkg['flutter_package']}: no main widget" diff --git a/tests/test_validators.py b/tests/test_validators.py new file mode 100644 index 0000000..c46964e --- /dev/null +++ b/tests/test_validators.py @@ -0,0 +1,89 @@ +from flet_pkg.core.validators import ( + derive_names, + validate_control_name, + validate_flutter_package, + validate_package_name, + validate_project_name, +) + + +class TestValidatePackageName: + def test_valid(self): + assert validate_package_name("flet_onesignal") is None + + def test_valid_simple(self): + assert validate_package_name("mypkg") is None + + def test_invalid_uppercase(self): + assert validate_package_name("FletPkg") is not None + + def test_invalid_hyphen(self): + assert validate_package_name("flet-pkg") is not None + + def test_invalid_starts_with_number(self): + assert validate_package_name("1pkg") is not None + + def test_empty(self): + assert validate_package_name("") is not None + + +class TestValidateProjectName: + def test_valid(self): + assert validate_project_name("flet-onesignal") is None + + def test_invalid_uppercase(self): + assert validate_project_name("Flet-Onesignal") is not None + + def test_invalid_underscore(self): + assert validate_project_name("flet_onesignal") is not None + + +class TestValidateFlutterPackage: + def test_valid(self): + assert validate_flutter_package("onesignal_flutter") is None + + def test_invalid(self): + assert validate_flutter_package("OneSignal-Flutter") is not None + + +class TestValidateControlName: + def test_valid(self): + assert validate_control_name("OneSignal") is None + + def test_valid_simple(self): + assert validate_control_name("Spinkit") is None + + def test_invalid_lowercase(self): + assert validate_control_name("oneSignal") is not None + + def test_invalid_underscore(self): + assert validate_control_name("One_Signal") is not None + + +class TestDeriveNames: + def test_flutter_suffix(self): + names = derive_names("onesignal_flutter") + assert names.project_name == "flet-onesignal" + assert names.package_name == "flet_onesignal" + assert names.control_name == "Onesignal" + assert names.control_name_snake == "onesignal" + + def test_flutter_prefix(self): + names = derive_names("flutter_spinkit") + assert names.project_name == "flet-spinkit" + assert names.package_name == "flet_spinkit" + assert names.control_name == "Spinkit" + assert names.control_name_snake == "spinkit" + + def test_multi_word(self): + names = derive_names("google_maps_flutter") + assert names.project_name == "flet-google-maps" + assert names.package_name == "flet_google_maps" + assert names.control_name == "GoogleMaps" + assert names.control_name_snake == "google_maps" + + def test_no_suffix(self): + names = derive_names("audioplayers") + assert names.project_name == "flet-audioplayers" + assert names.package_name == "flet_audioplayers" + assert names.control_name == "Audioplayers" diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..03c703b --- /dev/null +++ b/uv.lock @@ -0,0 +1,4189 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "ag-ui-protocol" +version = "0.1.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/b5/fc0b65b561d00d88811c8a7d98ee735833f81554be244340950e7b65820c/ag_ui_protocol-0.1.13.tar.gz", hash = "sha256:811d7d7dcce4783dec252918f40b717ebfa559399bf6b071c4ba47c0c1e21bcb", size = 5671, upload-time = "2026-02-19T18:40:38.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/9f/b833c1ab1999da35ebad54841ae85d2c2764c931da9a6f52d8541b6901b2/ag_ui_protocol-0.1.13-py3-none-any.whl", hash = "sha256:1393fa894c1e8416efe184168a50689e760d05b32f4646eebb8ff423dddf8e8f", size = 8053, upload-time = "2026-02-19T18:40:37.27Z" }, +] + +[[package]] +name = "aiofile" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "caio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, +] + +[[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 = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/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/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950, upload-time = "2026-01-03T17:29:13.002Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099, upload-time = "2026-01-03T17:29:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072, upload-time = "2026-01-03T17:29:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588, upload-time = "2026-01-03T17:29:18.539Z" }, + { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334, upload-time = "2026-01-03T17:29:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656, upload-time = "2026-01-03T17:29:22.531Z" }, + { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625, upload-time = "2026-01-03T17:29:24.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604, upload-time = "2026-01-03T17:29:26.099Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370, upload-time = "2026-01-03T17:29:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023, upload-time = "2026-01-03T17:29:30.002Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680, upload-time = "2026-01-03T17:29:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407, upload-time = "2026-01-03T17:29:33.392Z" }, + { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047, upload-time = "2026-01-03T17:29:34.855Z" }, + { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264, upload-time = "2026-01-03T17:29:36.389Z" }, + { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275, upload-time = "2026-01-03T17:29:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053, upload-time = "2026-01-03T17:29:40.074Z" }, + { url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687, upload-time = "2026-01-03T17:29:41.819Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { 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" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-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.84.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/ea/0869d6df9ef83dcf393aeefc12dd81677d091c6ffc86f783e51cf44062f2/anthropic-0.84.0.tar.gz", hash = "sha256:72f5f90e5aebe62dca316cb013629cfa24996b0f5a4593b8c3d712bc03c43c37", size = 539457, upload-time = "2026-02-25T05:22:38.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/ca/218fa25002a332c0aa149ba18ffc0543175998b1f65de63f6d106689a345/anthropic-0.84.0-py3-none-any.whl", hash = "sha256:861c4c50f91ca45f942e091d83b60530ad6d4f98733bfe648065364da05d29e7", size = 455156, upload-time = "2026-02-25T05:22:40.468Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +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 = "argcomplete" +version = "3.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "authlib" +version = "1.6.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "backrefs" +version = "6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", size = 398787, upload-time = "2026-02-16T19:10:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", size = 400747, upload-time = "2026-02-16T19:10:09.791Z" }, + { url = "https://files.pythonhosted.org/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", size = 412602, upload-time = "2026-02-16T19:10:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + +[[package]] +name = "boto3" +version = "1.42.59" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/4e/499cb52aaee9468c346bcc1158965e24e72b4e2a20052725b680e0ac949b/boto3-1.42.59.tar.gz", hash = "sha256:6c4a14a4eb37b58a9048901bdeefbe1c529638b73e8f55413319a25f010ca211", size = 112725, upload-time = "2026-02-27T20:25:33.228Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/c0/22d868b9408dc5a33935a72896ec8d638b2766c459668d1b37c3e5ac2066/boto3-1.42.59-py3-none-any.whl", hash = "sha256:7a66e3e8e2087ea4403e135e9de592e6d63fc9a91080d8dac415bb74df873a72", size = 140557, upload-time = "2026-02-27T20:25:31.774Z" }, +] + +[[package]] +name = "botocore" +version = "1.42.59" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/ae/50fb33bdf1911c216d50f98d989dd032a506f054cf829ebd737c6fa7e3e6/botocore-1.42.59.tar.gz", hash = "sha256:5314f19e1da8fc0ebc41bdb8bbe17c9a7397d87f4d887076ac8bdef972a34138", size = 14950271, upload-time = "2026-02-27T20:25:20.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/df/9d52819e0d804ead073d53ab1823bc0f0cb172a250fba31107b0b43fbb04/botocore-1.42.59-py3-none-any.whl", hash = "sha256:d2f2ff7ecc31e86ef46b5daee112cfbca052c13801285fb23af909f7bff5b657", size = 14619293, upload-time = "2026-02-27T20:25:17.455Z" }, +] + +[[package]] +name = "cachetools" +version = "7.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/c7/342b33cc6877eebc6c9bb45cb9f78e170e575839699f6f3cc96050176431/cachetools-7.0.2.tar.gz", hash = "sha256:7e7f09a4ca8b791d8bb4864afc71e9c17e607a28e6839ca1a644253c97dbeae0", size = 36983, upload-time = "2026-03-02T19:45:16.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/04/4b6968e77c110f12da96fdbfcb39c6557c2e5e81bd7afcf8ed893d5bc588/cachetools-7.0.2-py3-none-any.whl", hash = "sha256:938dcad184827c5e94928c4fd5526e2b46692b7fb1ae94472da9131d0299343c", size = 13793, upload-time = "2026-03-02T19:45:15.495Z" }, +] + +[[package]] +name = "caio" +version = "0.9.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/80/ea4ead0c5d52a9828692e7df20f0eafe8d26e671ce4883a0a146bb91049e/caio-0.9.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ca6c8ecda611478b6016cb94d23fd3eb7124852b985bdec7ecaad9f3116b9619", size = 36836, upload-time = "2025-12-26T15:22:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/36715c97c873649d1029001578f901b50250916295e3dddf20c865438865/caio-0.9.25-cp310-cp310-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db9b5681e4af8176159f0d6598e73b2279bb661e718c7ac23342c550bd78c241", size = 79695, upload-time = "2025-12-26T15:22:18.818Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ab/07080ecb1adb55a02cbd8ec0126aa8e43af343ffabb6a71125b42670e9a1/caio-0.9.25-cp310-cp310-manylinux_2_34_aarch64.whl", hash = "sha256:bf61d7d0c4fd10ffdd98ca47f7e8db4d7408e74649ffaf4bef40b029ada3c21b", size = 79457, upload-time = "2026-03-04T22:08:16.024Z" }, + { url = "https://files.pythonhosted.org/packages/88/95/dd55757bb671eb4c376e006c04e83beb413486821f517792ea603ef216e9/caio-0.9.25-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:ab52e5b643f8bbd64a0605d9412796cd3464cb8ca88593b13e95a0f0b10508ae", size = 77705, upload-time = "2026-03-04T22:08:17.202Z" }, + { url = "https://files.pythonhosted.org/packages/ec/90/543f556fcfcfa270713eef906b6352ab048e1e557afec12925c991dc93c2/caio-0.9.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6956d9e4a27021c8bd6c9677f3a59eb1d820cc32d0343cea7961a03b1371965", size = 36839, upload-time = "2025-12-26T15:21:40.267Z" }, + { url = "https://files.pythonhosted.org/packages/51/3b/36f3e8ec38dafe8de4831decd2e44c69303d2a3892d16ceda42afed44e1b/caio-0.9.25-cp311-cp311-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf84bfa039f25ad91f4f52944452a5f6f405e8afab4d445450978cd6241d1478", size = 80255, upload-time = "2025-12-26T15:22:20.271Z" }, + { url = "https://files.pythonhosted.org/packages/df/ce/65e64867d928e6aff1b4f0e12dba0ef6d5bf412c240dc1df9d421ac10573/caio-0.9.25-cp311-cp311-manylinux_2_34_aarch64.whl", hash = "sha256:ae3d62587332bce600f861a8de6256b1014d6485cfd25d68c15caf1611dd1f7c", size = 80052, upload-time = "2026-03-04T22:08:20.402Z" }, + { url = "https://files.pythonhosted.org/packages/46/90/e278863c47e14ec58309aa2e38a45882fbe67b4cc29ec9bc8f65852d3e45/caio-0.9.25-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:fc220b8533dcf0f238a6b1a4a937f92024c71e7b10b5a2dfc1c73604a25709bc", size = 78273, upload-time = "2026-03-04T22:08:21.368Z" }, + { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/8a1b580875303500a9c12b9e0af58cb82e47f5bcf888c2457742a138273c/caio-0.9.25-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb", size = 81502, upload-time = "2026-03-04T22:08:22.381Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/0fe770b8ffc8362c48134d1592d653a81a3d8748d764bec33864db36319d/caio-0.9.25-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69", size = 80200, upload-time = "2026-03-04T22:08:23.382Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" }, + { url = "https://files.pythonhosted.org/packages/87/a4/e534cf7d2d0e8d880e25dd61e8d921ffcfe15bd696734589826f5a2df727/caio-0.9.25-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:628a630eb7fb22381dd8e3c8ab7f59e854b9c806639811fc3f4310c6bd711d79", size = 81565, upload-time = "2026-03-04T22:08:27.483Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ed/bf81aeac1d290017e5e5ac3e880fd56ee15e50a6d0353986799d1bc5cfd5/caio-0.9.25-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0ba16aa605ccb174665357fc729cf500679c2d94d5f1458a6f0d5ca48f2060a7", size = 80071, upload-time = "2026-03-04T22:08:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, +] + +[[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/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.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 = "cohere" +version = "5.20.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastavro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "pydantic-core" }, + { name = "requests" }, + { name = "tokenizers" }, + { name = "types-requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/0b/96e2b55a0114ed9d69b3154565f54b764e7530735426290b000f467f4c0f/cohere-5.20.7.tar.gz", hash = "sha256:997ed85fabb3a1e4a4c036fdb520382e7bfa670db48eb59a026803b6f7061dbb", size = 184986, upload-time = "2026-02-25T01:22:18.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/86/dc991a75e3b9c2007b90dbfaf7f36fdb2457c216f799e26ce0474faf0c1f/cohere-5.20.7-py3-none-any.whl", hash = "sha256:043fef2a12c30c07e9b2c1f0b869fd66ffd911f58d1492f87e901c4190a65914", size = 323389, upload-time = "2026-02-25T01:22:16.902Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, + { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, + { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, + { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, + { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, + { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, +] + +[[package]] +name = "cyclopts" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser" }, + { name = "rich" }, + { name = "rich-rst" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/5c/88a4068c660a096bbe87efc5b7c190080c9e86919c36ec5f092cb08d852f/cyclopts-4.6.0.tar.gz", hash = "sha256:483c4704b953ea6da742e8de15972f405d2e748d19a848a4d61595e8e5360ee5", size = 162724, upload-time = "2026-02-23T15:44:49.286Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/eb/1e8337755a70dc7d7ff10a73dc8f20e9352c9ad6c2256ed863ac95cd3539/cyclopts-4.6.0-py3-none-any.whl", hash = "sha256:0a891cb55bfd79a3cdce024db8987b33316aba11071e5258c21ac12a640ba9f2", size = 200518, upload-time = "2026-02-23T15:44:47.854Z" }, +] + +[[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 = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[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 = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "eval-type-backport" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/a3/cafafb4558fd638aadfe4121dc6cefb8d743368c085acb2f521df0f3d9d7/eval_type_backport-0.3.1.tar.gz", hash = "sha256:57e993f7b5b69d271e37482e62f74e76a0276c82490cf8e4f0dffeb6b332d5ed", size = 9445, upload-time = "2025-12-02T11:51:42.987Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/22/fdc2e30d43ff853720042fa15baa3e6122722be1a7950a98233ebb55cd71/eval_type_backport-0.3.1-py3-none-any.whl", hash = "sha256:279ab641905e9f11129f56a8a78f493518515b83402b860f6f06dd7c011fdfa8", size = 6063, upload-time = "2025-12-02T11:51:41.665Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fastavro" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/8b/fa2d3287fd2267be6261d0177c6809a7fa12c5600ddb33490c8dc29e77b2/fastavro-1.12.1.tar.gz", hash = "sha256:2f285be49e45bc047ab2f6bed040bb349da85db3f3c87880e4b92595ea093b2b", size = 1025661, upload-time = "2025-10-10T15:40:55.41Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/a0/077fd7cbfc143152cb96780cb592ed6cb6696667d8bc1b977745eb2255a8/fastavro-1.12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:00650ca533907361edda22e6ffe8cf87ab2091c5d8aee5c8000b0f2dcdda7ed3", size = 1000335, upload-time = "2025-10-10T15:40:59.834Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ae/a115e027f3a75df237609701b03ecba0b7f0aa3d77fe0161df533fde1eb7/fastavro-1.12.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac76d6d95f909c72ee70d314b460b7e711d928845771531d823eb96a10952d26", size = 3221067, upload-time = "2025-10-10T15:41:04.399Z" }, + { url = "https://files.pythonhosted.org/packages/94/4e/c4991c3eec0175af9a8a0c161b88089cb7bf7fe353b3e3be1bc4cf9036b2/fastavro-1.12.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55eef18c41d4476bd32a82ed5dd86aabc3f614e1b66bdb09ffa291612e1670", size = 3228979, upload-time = "2025-10-10T15:41:06.738Z" }, + { url = "https://files.pythonhosted.org/packages/21/0c/f2afb8eaea38799ccb1ed07d68bf2659f2e313f1902bbd36774cf6a1bef9/fastavro-1.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81563e1f93570e6565487cdb01ba241a36a00e58cff9c5a0614af819d1155d8f", size = 3160740, upload-time = "2025-10-10T15:41:08.731Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1a/f4d367924b40b86857862c1fa65f2afba94ddadf298b611e610a676a29e5/fastavro-1.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bec207360f76f0b3de540758a297193c5390e8e081c43c3317f610b1414d8c8f", size = 3235787, upload-time = "2025-10-10T15:41:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/8db9331896e3dfe4f71b2b3c23f2e97fbbfd90129777467ca9f8bafccb74/fastavro-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:c0390bfe4a9f8056a75ac6785fbbff8f5e317f5356481d2e29ec980877d2314b", size = 449350, upload-time = "2025-10-10T15:41:12.104Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e9/31c64b47cefc0951099e7c0c8c8ea1c931edd1350f34d55c27cbfbb08df1/fastavro-1.12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b632b713bc5d03928a87d811fa4a11d5f25cd43e79c161e291c7d3f7aa740fd", size = 1016585, upload-time = "2025-10-10T15:41:13.717Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/111560775b548f5d8d828c1b5285ff90e2d2745643fb80ecbf115344eea4/fastavro-1.12.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa7ab3769beadcebb60f0539054c7755f63bd9cf7666e2c15e615ab605f89a8", size = 3404629, upload-time = "2025-10-10T15:41:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/b0/07/6bb93cb963932146c2b6c5c765903a0a547ad9f0f8b769a4a9aad8c06369/fastavro-1.12.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123fb221df3164abd93f2d042c82f538a1d5a43ce41375f12c91ce1355a9141e", size = 3428594, upload-time = "2025-10-10T15:41:17.779Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8115ec36b584197ea737ec79e3499e1f1b640b288d6c6ee295edd13b80f6/fastavro-1.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:632a4e3ff223f834ddb746baae0cc7cee1068eb12c32e4d982c2fee8a5b483d0", size = 3344145, upload-time = "2025-10-10T15:41:19.89Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9e/a7cebb3af967e62539539897c10138fa0821668ec92525d1be88a9cd3ee6/fastavro-1.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e6caf4e7a8717d932a3b1ff31595ad169289bbe1128a216be070d3a8391671", size = 3431942, upload-time = "2025-10-10T15:41:22.076Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d1/7774ddfb8781c5224294c01a593ebce2ad3289b948061c9701bd1903264d/fastavro-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:b91a0fe5a173679a6c02d53ca22dcaad0a2c726b74507e0c1c2e71a7c3f79ef9", size = 450542, upload-time = "2025-10-10T15:41:23.333Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f0/10bd1a3d08667fa0739e2b451fe90e06df575ec8b8ba5d3135c70555c9bd/fastavro-1.12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:509818cb24b98a804fc80be9c5fed90f660310ae3d59382fc811bfa187122167", size = 1009057, upload-time = "2025-10-10T15:41:24.556Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/0d985bc99e1fa9e74c636658000ba38a5cd7f5ab2708e9c62eaf736ecf1a/fastavro-1.12.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089e155c0c76e0d418d7e79144ce000524dd345eab3bc1e9c5ae69d500f71b14", size = 3391866, upload-time = "2025-10-10T15:41:26.882Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9e/b4951dc84ebc34aac69afcbfbb22ea4a91080422ec2bfd2c06076ff1d419/fastavro-1.12.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44cbff7518901c91a82aab476fcab13d102e4999499df219d481b9e15f61af34", size = 3458005, upload-time = "2025-10-10T15:41:29.017Z" }, + { url = "https://files.pythonhosted.org/packages/af/f8/5a8df450a9f55ca8441f22ea0351d8c77809fc121498b6970daaaf667a21/fastavro-1.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a275e48df0b1701bb764b18a8a21900b24cf882263cb03d35ecdba636bbc830b", size = 3295258, upload-time = "2025-10-10T15:41:31.564Z" }, + { url = "https://files.pythonhosted.org/packages/99/b2/40f25299111d737e58b85696e91138a66c25b7334f5357e7ac2b0e8966f8/fastavro-1.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2de72d786eb38be6b16d556b27232b1bf1b2797ea09599507938cdb7a9fe3e7c", size = 3430328, upload-time = "2025-10-10T15:41:33.689Z" }, + { url = "https://files.pythonhosted.org/packages/e0/07/85157a7c57c5f8b95507d7829b5946561e5ee656ff80e9dd9a757f53ddaf/fastavro-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:9090f0dee63fe022ee9cc5147483366cc4171c821644c22da020d6b48f576b4f", size = 444140, upload-time = "2025-10-10T15:41:34.902Z" }, + { url = "https://files.pythonhosted.org/packages/bb/57/26d5efef9182392d5ac9f253953c856ccb66e4c549fd3176a1e94efb05c9/fastavro-1.12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78df838351e4dff9edd10a1c41d1324131ffecbadefb9c297d612ef5363c049a", size = 1000599, upload-time = "2025-10-10T15:41:36.554Z" }, + { url = "https://files.pythonhosted.org/packages/33/cb/8ab55b21d018178eb126007a56bde14fd01c0afc11d20b5f2624fe01e698/fastavro-1.12.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:780476c23175d2ae457c52f45b9ffa9d504593499a36cd3c1929662bf5b7b14b", size = 3335933, upload-time = "2025-10-10T15:41:39.07Z" }, + { url = "https://files.pythonhosted.org/packages/fe/03/9c94ec9bf873eb1ffb0aa694f4e71940154e6e9728ddfdc46046d7e8ced4/fastavro-1.12.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0714b285160fcd515eb0455540f40dd6dac93bdeacdb03f24e8eac3d8aa51f8d", size = 3402066, upload-time = "2025-10-10T15:41:41.608Z" }, + { url = "https://files.pythonhosted.org/packages/75/c8/cb472347c5a584ccb8777a649ebb28278fccea39d005fc7df19996f41df8/fastavro-1.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a8bc2dcec5843d499f2489bfe0747999108f78c5b29295d877379f1972a3d41a", size = 3240038, upload-time = "2025-10-10T15:41:43.743Z" }, + { url = "https://files.pythonhosted.org/packages/e1/77/569ce9474c40304b3a09e109494e020462b83e405545b78069ddba5f614e/fastavro-1.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3b1921ac35f3d89090a5816b626cf46e67dbecf3f054131f84d56b4e70496f45", size = 3369398, upload-time = "2025-10-10T15:41:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1f/9589e35e9ea68035385db7bdbf500d36b8891db474063fb1ccc8215ee37c/fastavro-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:5aa777b8ee595b50aa084104cd70670bf25a7bbb9fd8bb5d07524b0785ee1699", size = 444220, upload-time = "2025-10-10T15:41:47.39Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d2/78435fe737df94bd8db2234b2100f5453737cffd29adee2504a2b013de84/fastavro-1.12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c3d67c47f177e486640404a56f2f50b165fe892cc343ac3a34673b80cc7f1dd6", size = 1086611, upload-time = "2025-10-10T15:41:48.818Z" }, + { url = "https://files.pythonhosted.org/packages/b6/be/428f99b10157230ddac77ec8cc167005b29e2bd5cbe228345192bb645f30/fastavro-1.12.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5217f773492bac43dae15ff2931432bce2d7a80be7039685a78d3fab7df910bd", size = 3541001, upload-time = "2025-10-10T15:41:50.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/08/a2eea4f20b85897740efe44887e1ac08f30dfa4bfc3de8962bdcbb21a5a1/fastavro-1.12.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:469fecb25cba07f2e1bfa4c8d008477cd6b5b34a59d48715e1b1a73f6160097d", size = 3432217, upload-time = "2025-10-10T15:41:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/87/bb/b4c620b9eb6e9838c7f7e4b7be0762834443adf9daeb252a214e9ad3178c/fastavro-1.12.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d71c8aa841ef65cfab709a22bb887955f42934bced3ddb571e98fdbdade4c609", size = 3366742, upload-time = "2025-10-10T15:41:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d1/e69534ccdd5368350646fea7d93be39e5f77c614cca825c990bd9ca58f67/fastavro-1.12.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b81fc04e85dfccf7c028e0580c606e33aa8472370b767ef058aae2c674a90746", size = 3383743, upload-time = "2025-10-10T15:41:57.68Z" }, + { url = "https://files.pythonhosted.org/packages/58/54/b7b4a0c3fb5fcba38128542da1b26c4e6d69933c923f493548bdfd63ab6a/fastavro-1.12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9445da127751ba65975d8e4bdabf36bfcfdad70fc35b2d988e3950cce0ec0e7c", size = 1001377, upload-time = "2025-10-10T15:41:59.241Z" }, + { url = "https://files.pythonhosted.org/packages/1e/4f/0e589089c7df0d8f57d7e5293fdc34efec9a3b758a0d4d0c99a7937e2492/fastavro-1.12.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed924233272719b5d5a6a0b4d80ef3345fc7e84fc7a382b6232192a9112d38a6", size = 3320401, upload-time = "2025-10-10T15:42:01.682Z" }, + { url = "https://files.pythonhosted.org/packages/f9/19/260110d56194ae29d7e423a336fccea8bcd103196d00f0b364b732bdb84e/fastavro-1.12.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3616e2f0e1c9265e92954fa099db79c6e7817356d3ff34f4bcc92699ae99697c", size = 3350894, upload-time = "2025-10-10T15:42:04.073Z" }, + { url = "https://files.pythonhosted.org/packages/d0/96/58b0411e8be9694d5972bee3167d6c1fd1fdfdf7ce253c1a19a327208f4f/fastavro-1.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cb0337b42fd3c047fcf0e9b7597bd6ad25868de719f29da81eabb6343f08d399", size = 3229644, upload-time = "2025-10-10T15:42:06.221Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/38660660eac82c30471d9101f45b3acfdcbadfe42d8f7cdb129459a45050/fastavro-1.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:64961ab15b74b7c168717bbece5660e0f3d457837c3cc9d9145181d011199fa7", size = 3329704, upload-time = "2025-10-10T15:42:08.384Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a9/1672910f458ecb30b596c9e59e41b7c00309b602a0494341451e92e62747/fastavro-1.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:792356d320f6e757e89f7ac9c22f481e546c886454a6709247f43c0dd7058004", size = 452911, upload-time = "2025-10-10T15:42:09.795Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/2e15d0938ded1891b33eff252e8500605508b799c2e57188a933f0bd744c/fastavro-1.12.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:120aaf82ac19d60a1016afe410935fe94728752d9c2d684e267e5b7f0e70f6d9", size = 3541999, upload-time = "2025-10-10T15:42:11.794Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1c/6dfd082a205be4510543221b734b1191299e6a1810c452b6bc76dfa6968e/fastavro-1.12.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6a3462934b20a74f9ece1daa49c2e4e749bd9a35fa2657b53bf62898fba80f5", size = 3433972, upload-time = "2025-10-10T15:42:14.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/9de694625a1a4b727b1ad0958d220cab25a9b6cf7f16a5c7faa9ea7b2261/fastavro-1.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1f81011d54dd47b12437b51dd93a70a9aa17b61307abf26542fc3c13efbc6c51", size = 3368752, upload-time = "2025-10-10T15:42:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/fa/93/b44f67589e4d439913dab6720f7e3507b0fa8b8e56d06f6fc875ced26afb/fastavro-1.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43ded16b3f4a9f1a42f5970c2aa618acb23ea59c4fcaa06680bdf470b255e5a8", size = 3386636, upload-time = "2025-10-10T15:42:18.974Z" }, +] + +[[package]] +name = "fastmcp" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib" }, + { name = "cyclopts" }, + { name = "exceptiongroup" }, + { name = "httpx" }, + { name = "jsonref" }, + { name = "jsonschema-path" }, + { name = "mcp" }, + { name = "openapi-pydantic" }, + { name = "opentelemetry-api" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, + { name = "pydantic", extra = ["email"] }, + { name = "pyperclip" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "uncalled-for" }, + { name = "uvicorn" }, + { name = "watchfiles" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/70/862026c4589441f86ad3108f05bfb2f781c6b322ad60a982f40b303b47d7/fastmcp-3.1.0.tar.gz", hash = "sha256:e25264794c734b9977502a51466961eeecff92a0c2f3b49c40c070993628d6d0", size = 17347083, upload-time = "2026-03-03T02:43:11.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/07/516f5b20d88932e5a466c2216b628e5358a71b3a9f522215607c3281de05/fastmcp-3.1.0-py3-none-any.whl", hash = "sha256:b1f73b56fd3b0cb2bd9e2a144fc650d5cc31587ed129d996db7710e464ae8010", size = 633749, upload-time = "2026-03-03T02:43:09.06Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158, upload-time = "2026-03-01T15:08:45.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427, upload-time = "2026-03-01T15:08:44.593Z" }, +] + +[[package]] +name = "flet-pkg" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "httpx" }, + { name = "jinja2" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "typer" }, +] + +[package.optional-dependencies] +ai = [ + { name = "pydantic-ai" }, +] +mcp = [ + { name = "mcp", extra = ["cli"] }, +] + +[package.dev-dependencies] +dev = [ + { name = "mcp", extra = ["cli"] }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, +] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "pymdown-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.27.0" }, + { name = "jinja2", specifier = ">=3.1.0" }, + { name = "mcp", extras = ["cli"], marker = "extra == 'mcp'", specifier = ">=1.0.0" }, + { name = "pydantic-ai", marker = "extra == 'ai'", specifier = ">=1.0.0" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "rich", specifier = ">=13.0.0" }, + { name = "typer", specifier = ">=0.12.0" }, +] +provides-extras = ["ai", "mcp"] + +[package.metadata.requires-dev] +dev = [ + { name = "mcp", extras = ["cli"], specifier = ">=1.0.0" }, + { name = "pytest", specifier = ">=8.0" }, + { name = "pytest-asyncio", specifier = ">=0.24.0" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, +] +docs = [ + { name = "mkdocs", specifier = ">=1.6.0" }, + { name = "mkdocs-material", specifier = ">=9.6.0" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.29.0" }, + { name = "pymdown-extensions", specifier = ">=10.14.0" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + +[[package]] +name = "genai-prices" +version = "0.0.55" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/67/de9d9be180db6d80b298c281dff71502095c0776d7cc9286f486f667f61a/genai_prices-0.0.55.tar.gz", hash = "sha256:8692c65d0deefe2ad0680d71841eb12822a35945a6060d2b6adbcbdf4945e1cb", size = 59987, upload-time = "2026-02-26T17:56:41.467Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/98/66a06b82a5c840f896490d5ef9c7691776b147589f2e8d2fa66c67a3db9c/genai_prices-0.0.55-py3-none-any.whl", hash = "sha256:ccd795c90c926b3c71066bf5656f14c67fc11fdba6d71e072c7fb4fa311e1b12", size = 62603, upload-time = "2026-02-26T17:56:40.502Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "google-auth" +version = "2.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-genai" +version = "1.65.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/f9/cc1191c2540d6a4e24609a586c4ed45d2db57cfef47931c139ee70e5874a/google_genai-1.65.0.tar.gz", hash = "sha256:d470eb600af802d58a79c7f13342d9ea0d05d965007cae8f76c7adff3d7a4750", size = 497206, upload-time = "2026-02-26T00:20:33.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/3c/3fea4e7c91357c71782d7dcaad7a2577d636c90317e003386893c25bc62c/google_genai-1.65.0-py3-none-any.whl", hash = "sha256:68c025205856919bc03edb0155c11b4b833810b7ce17ad4b7a9eeba5158f6c44", size = 724429, upload-time = "2026-02-26T00:20:32.186Z" }, +] + +[[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 = "griffelib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, +] + +[[package]] +name = "groq" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/12/f4099a141677fcd2ed79dcc1fcec431e60c52e0e90c9c5d935f0ffaf8c0e/groq-1.0.0.tar.gz", hash = "sha256:66cb7bb729e6eb644daac7ce8efe945e99e4eb33657f733ee6f13059ef0c25a9", size = 146068, upload-time = "2025-12-17T23:34:23.115Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/88/3175759d2ef30406ea721f4d837bfa1ba4339fde3b81ba8c5640a96ed231/groq-1.0.0-py3-none-any.whl", hash = "sha256:6e22bf92ffad988f01d2d4df7729add66b8fd5dbfb2154b5bbf3af245b72c731", size = 138292, upload-time = "2025-12-17T23:34:21.957Z" }, +] + +[[package]] +name = "grpcio" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/a8/690a085b4d1fe066130de97a87de32c45062cf2ecd218df9675add895550/grpcio-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5", size = 5946986, upload-time = "2026-02-06T09:54:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1b/e5213c5c0ced9d2d92778d30529ad5bb2dcfb6c48c4e2d01b1f302d33d64/grpcio-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2", size = 11816533, upload-time = "2026-02-06T09:54:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/1ba32dccf0a324cc5ace744c44331e300b000a924bf14840f948c559ede7/grpcio-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d", size = 6519964, upload-time = "2026-02-06T09:54:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f5/c0e178721b818072f2e8b6fde13faaba942406c634009caf065121ce246b/grpcio-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb", size = 7198058, upload-time = "2026-02-06T09:54:42.389Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/40d43c91ae9cd667edc960135f9f08e58faa1576dc95af29f66ec912985f/grpcio-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7", size = 6727212, upload-time = "2026-02-06T09:54:44.91Z" }, + { url = "https://files.pythonhosted.org/packages/ed/88/9da42eed498f0efcfcd9156e48ae63c0cde3bea398a16c99fb5198c885b6/grpcio-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec", size = 7300845, upload-time = "2026-02-06T09:54:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/23/3f/1c66b7b1b19a8828890e37868411a6e6925df5a9030bfa87ab318f34095d/grpcio-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a", size = 8284605, upload-time = "2026-02-06T09:54:50.475Z" }, + { url = "https://files.pythonhosted.org/packages/94/c4/ca1bd87394f7b033e88525384b4d1e269e8424ab441ea2fba1a0c5b50986/grpcio-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813", size = 7726672, upload-time = "2026-02-06T09:54:53.11Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/f16e487d4cc65ccaf670f6ebdd1a17566b965c74fc3d93999d3b2821e052/grpcio-1.78.0-cp310-cp310-win32.whl", hash = "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de", size = 4076715, upload-time = "2026-02-06T09:54:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/2a/32/4ce60d94e242725fd3bcc5673c04502c82a8e87b21ea411a63992dc39f8f/grpcio-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf", size = 4799157, upload-time = "2026-02-06T09:54:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, + { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, + { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, + { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, + { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, + { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, + { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, + { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/cb/9bb543bd987ffa1ee48202cc96a756951b734b79a542335c566148ade36c/hf_xet-1.3.2.tar.gz", hash = "sha256:e130ee08984783d12717444e538587fa2119385e5bd8fc2bb9f930419b73a7af", size = 643646, upload-time = "2026-02-27T17:26:08.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/75/462285971954269432aad2e7938c5c7ff9ec7d60129cec542ab37121e3d6/hf_xet-1.3.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:335a8f36c55fd35a92d0062f4e9201b4015057e62747b7e7001ffb203c0ee1d2", size = 3761019, upload-time = "2026-02-27T17:25:49.441Z" }, + { url = "https://files.pythonhosted.org/packages/35/56/987b0537ddaf88e17192ea09afa8eca853e55f39a4721578be436f8409df/hf_xet-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c1ae4d3a716afc774e66922f3cac8206bfa707db13f6a7e62dfff74bfc95c9a8", size = 3521565, upload-time = "2026-02-27T17:25:47.469Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5c/7e4a33a3d689f77761156cc34558047569e54af92e4d15a8f493229f6767/hf_xet-1.3.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6dbdf231efac0b9b39adcf12a07f0c030498f9212a18e8c50224d0e84ab803d", size = 4176494, upload-time = "2026-02-27T17:25:40.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b3/71e856bf9d9a69b3931837e8bf22e095775f268c8edcd4a9e8c355f92484/hf_xet-1.3.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c1980abfb68ecf6c1c7983379ed7b1e2b49a1aaf1a5aca9acc7d48e5e2e0a961", size = 3955601, upload-time = "2026-02-27T17:25:38.376Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/aecf97b3f0a981600a67ff4db15e2d433389d698a284bb0ea5d8fcdd6f7f/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1c88fbd90ad0d27c46b77a445f0a436ebaa94e14965c581123b68b1c52f5fd30", size = 4154770, upload-time = "2026-02-27T17:25:56.756Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e1/3af961f71a40e09bf5ee909842127b6b00f5ab4ee3817599dc0771b79893/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:35b855024ca37f2dd113ac1c08993e997fbe167b9d61f9ef66d3d4f84015e508", size = 4394161, upload-time = "2026-02-27T17:25:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c3/859509bade9178e21b8b1db867b8e10e9f817ab9ac1de77cb9f461ced765/hf_xet-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:31612ba0629046e425ba50375685a2586e11fb9144270ebabd75878c3eaf6378", size = 3637377, upload-time = "2026-02-27T17:26:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/05/7f/724cfbef4da92d577b71f68bf832961c8919f36c60d28d289a9fc9d024d4/hf_xet-1.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:433c77c9f4e132b562f37d66c9b22c05b5479f243a1f06a120c1c06ce8b1502a", size = 3497875, upload-time = "2026-02-27T17:26:09.034Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/9d54c1ae1d05fb704f977eca1671747babf1957f19f38ae75c5933bc2dc1/hf_xet-1.3.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:c34e2c7aefad15792d57067c1c89b2b02c1bbaeabd7f8456ae3d07b4bbaf4094", size = 3761076, upload-time = "2026-02-27T17:25:55.42Z" }, + { url = "https://files.pythonhosted.org/packages/f2/8a/08a24b6c6f52b5d26848c16e4b6d790bb810d1bf62c3505bed179f7032d3/hf_xet-1.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4bc995d6c41992831f762096020dc14a65fdf3963f86ffed580b596d04de32e3", size = 3521745, upload-time = "2026-02-27T17:25:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/b5/db/a75cf400dd8a1a8acf226a12955ff6ee999f272dfc0505bafd8079a61267/hf_xet-1.3.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:959083c89dee30f7d6f890b36cdadda823386c4de63b1a30384a75bfd2ae995d", size = 4176301, upload-time = "2026-02-27T17:25:46.044Z" }, + { url = "https://files.pythonhosted.org/packages/01/40/6c4c798ffdd83e740dd3925c4e47793b07442a9efa3bc3866ba141a82365/hf_xet-1.3.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cfa760888633b08c01b398d212ce7e8c0d7adac6c86e4b20dfb2397d8acd78ee", size = 3955437, upload-time = "2026-02-27T17:25:44.703Z" }, + { url = "https://files.pythonhosted.org/packages/0c/09/9a3aa7c5f07d3e5cc57bb750d12a124ffa72c273a87164bd848f9ac5cc14/hf_xet-1.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3155a02e083aa21fd733a7485c7c36025e49d5975c8d6bda0453d224dd0b0ac4", size = 4154535, upload-time = "2026-02-27T17:26:05.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e0/831f7fa6d90cb47a230bc23284b502c700e1483bbe459437b3844cdc0776/hf_xet-1.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:91b1dc03c31cbf733d35dc03df7c5353686233d86af045e716f1e0ea4a2673cf", size = 4393891, upload-time = "2026-02-27T17:26:06.607Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/6ed472fdce7f8b70f5da6e3f05be76816a610063003bfd6d9cea0bbb58a3/hf_xet-1.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:211f30098512d95e85ad03ae63bd7dd2c4df476558a5095d09f9e38e78cbf674", size = 3637583, upload-time = "2026-02-27T17:26:17.349Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/a069edc4570b3f8e123c0b80fadc94530f3d7b01394e1fc1bb223339366c/hf_xet-1.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:4a6817c41de7c48ed9270da0b02849347e089c5ece9a0e72ae4f4b3a57617f82", size = 3497977, upload-time = "2026-02-27T17:26:14.966Z" }, + { url = "https://files.pythonhosted.org/packages/d8/28/dbb024e2e3907f6f3052847ca7d1a2f7a3972fafcd53ff79018977fcb3e4/hf_xet-1.3.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f93b7595f1d8fefddfede775c18b5c9256757824f7f6832930b49858483cd56f", size = 3763961, upload-time = "2026-02-27T17:25:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/e4/71/b99aed3823c9d1795e4865cf437d651097356a3f38c7d5877e4ac544b8e4/hf_xet-1.3.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a85d3d43743174393afe27835bde0cd146e652b5fcfdbcd624602daef2ef3259", size = 3526171, upload-time = "2026-02-27T17:25:50.968Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/907890ce6ef5598b5920514f255ed0a65f558f820515b18db75a51b2f878/hf_xet-1.3.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7c2a054a97c44e136b1f7f5a78f12b3efffdf2eed3abc6746fc5ea4b39511633", size = 4180750, upload-time = "2026-02-27T17:25:43.125Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ad/bc7f41f87173d51d0bce497b171c4ee0cbde1eed2d7b4216db5d0ada9f50/hf_xet-1.3.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:06b724a361f670ae557836e57801b82c75b534812e351a87a2c739f77d1e0635", size = 3961035, upload-time = "2026-02-27T17:25:41.837Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/600f4dda40c4a33133404d9fe644f1d35ff2d9babb4d0435c646c63dd107/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:305f5489d7241a47e0458ef49334be02411d1d0f480846363c1c8084ed9916f7", size = 4161378, upload-time = "2026-02-27T17:26:00.365Z" }, + { url = "https://files.pythonhosted.org/packages/00/b3/7bc1ff91d1ac18420b7ad1e169b618b27c00001b96310a89f8a9294fe509/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:06cdbde243c85f39a63b28e9034321399c507bcd5e7befdd17ed2ccc06dfe14e", size = 4398020, upload-time = "2026-02-27T17:26:03.977Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/99bfd948a3ed3620ab709276df3ad3710dcea61976918cce8706502927af/hf_xet-1.3.2-cp37-abi3-win_amd64.whl", hash = "sha256:9298b47cce6037b7045ae41482e703c471ce36b52e73e49f71226d2e8e5685a1", size = 3641624, upload-time = "2026-02-27T17:26:13.542Z" }, + { url = "https://files.pythonhosted.org/packages/cc/02/9a6e4ca1f3f73a164c0cd48e41b3cc56585dcc37e809250de443d673266f/hf_xet-1.3.2-cp37-abi3-win_arm64.whl", hash = "sha256:83d8ec273136171431833a6957e8f3af496bee227a0fe47c7b8b39c106d1749a", size = 3503976, upload-time = "2026-02-27T17:26:12.123Z" }, +] + +[[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 = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.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 = "huggingface-hub" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/76/b5efb3033d8499b17f9386beaf60f64c461798e1ee16d10bc9c0077beba5/huggingface_hub-1.5.0.tar.gz", hash = "sha256:f281838db29265880fb543de7a23b0f81d3504675de82044307ea3c6c62f799d", size = 695872, upload-time = "2026-02-26T15:35:32.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/74/2bc951622e2dbba1af9a460d93c51d15e458becd486e62c29cc0ccb08178/huggingface_hub-1.5.0-py3-none-any.whl", hash = "sha256:c9c0b3ab95a777fc91666111f3b3ede71c0cdced3614c553a64e98920585c4ee", size = 596261, upload-time = "2026-02-26T15:35:31.1Z" }, +] + +[[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 = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "invoke" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/bd/b461d3424a24c80490313fd77feeb666ca4f6a28c7e72713e3d9095719b4/invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707", size = 304762, upload-time = "2025-10-11T00:36:35.172Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/4b/b99e37f88336009971405cbb7630610322ed6fbfa31e1d7ab3fbf3049a2d/invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8", size = 160287, upload-time = "2025-10-11T00:36:33.703Z" }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[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.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" }, + { url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" }, + { url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" }, + { url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, + { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, + { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, + { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, +] + +[[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-path" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/b4/41315eea8301a5353bca3578792767135b8edbc081b20618a3f0b4d78307/jsonschema_path-0.4.4.tar.gz", hash = "sha256:4c55842890fc384262a59fb63a25c86cc0e2b059e929c18b851c1d19ef612026", size = 14923, upload-time = "2026-02-28T11:58:26.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/36/cb2cd6543776d02875de600f12fcd81611daf359544c9ad2abb12d3122a5/jsonschema_path-0.4.4-py3-none-any.whl", hash = "sha256:669bb69cb92cd4c54acf38ee2ff7c3d9ab6b69991698f7a2f17d2bb0e5c9c394", size = 19226, upload-time = "2026-02-28T11:58:25.143Z" }, +] + +[[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 = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "logfire" +version = "4.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "executing" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-sdk" }, + { name = "protobuf" }, + { name = "rich" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/43/374fc0e6ebe95209414cf743cc693f4ff2ad391fd0712445ed1f63245395/logfire-4.25.0.tar.gz", hash = "sha256:f9a6bf6d40fd3e2c2a86a364617246cadecbde620b4ecccb17c499140f1ebc13", size = 1049745, upload-time = "2026-02-19T15:27:28Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/cc/a3eb3a5fff27a6bfe2f626624c7c781322151f3228d4ea98c31003dc2d4c/logfire-4.25.0-py3-none-any.whl", hash = "sha256:1865b832e08c58a3fb0d21b24460ee9c6cbeff12db6038c508fb966699ce81c2", size = 298186, upload-time = "2026-02-19T15:27:23.324Z" }, +] + +[package.optional-dependencies] +httpx = [ + { name = "opentelemetry-instrumentation-httpx" }, +] + +[[package]] +name = "logfire-api" +version = "4.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/5c/026cec30d85394aec8f5f12d70edbe2d706837bc9a411bd71a542cedae50/logfire_api-4.25.0.tar.gz", hash = "sha256:7562d5adfe3987291039dddb21947c86cb9d832d068c87d9aa23db86ef07095b", size = 75853, upload-time = "2026-02-19T15:27:29.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/39/83414c0fadb4f11f90e6b80b631aa79f62a605664f0c4693e2ebc7ee73f3/logfire_api-4.25.0-py3-none-any.whl", hash = "sha256:0d607eb09ef5426e26f376ff277a8d401bc5b7b4178ea66db404e13c368494cf", size = 120473, upload-time = "2026-02-19T15:27:25.832Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "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/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { 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.26.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/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, +] + +[package.optional-dependencies] +cli = [ + { name = "python-dotenv" }, + { name = "typer" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mistralai" +version = "1.12.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eval-type-backport" }, + { name = "httpx" }, + { name = "invoke" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/12/c3476c53e907255b5f485f085ba50dd9a84b40fe662e9a888d6ded26fa7b/mistralai-1.12.4.tar.gz", hash = "sha256:e52b53bab58025dcd208eeac13e3c3df5778d4112eeca1f08124096c7738929f", size = 243129, upload-time = "2026-02-20T17:55:13.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/f9/98d825105c450b9c67c27026caa374112b7e466c18331601d02ca278a01b/mistralai-1.12.4-py3-none-any.whl", hash = "sha256:7b69fcbc306436491ad3377fbdead527c9f3a0ce145ec029bf04c6308ff2cca6", size = 509321, upload-time = "2026-02-20T17:55:15.27Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/b4/f900fcb8e6f510241e334ca401eddcb61ed880fb6572f7f32e4228472ca1/mkdocs_material-9.7.3.tar.gz", hash = "sha256:e5f0a18319699da7e78c35e4a8df7e93537a888660f61a86bd773a7134798f22", size = 4097748, upload-time = "2026-02-24T12:06:22.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/1b/16ad0193079bb8a15aa1d2620813a9cd15b18de150a4ea1b2c607fb4c74d/mkdocs_material-9.7.3-py3-none-any.whl", hash = "sha256:37ebf7b4788c992203faf2e71900be3c197c70a4be9b0d72aed537b08a91dd9d", size = 9305078, upload-time = "2026-02-24T12:06:19.155Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/62/0dfc5719514115bf1781f44b1d7f2a0923fcc01e9c5d7990e48a05c9ae5d/mkdocstrings-1.0.3.tar.gz", hash = "sha256:ab670f55040722b49bb45865b2e93b824450fb4aef638b00d7acb493a9020434", size = 100946, upload-time = "2026-02-07T14:31:40.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-py3-none-any.whl", hash = "sha256:0d66d18430c2201dc7fe85134277382baaa15e6b30979f3f3bdbabd6dbdb6046", size = 35523, upload-time = "2026-02-07T14:31:39.27Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, + { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, + { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, + { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, + { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, + { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "nexus-rpc" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/50/95d7bc91f900da5e22662c82d9bf0f72a4b01f2a552708bf2f43807707a1/nexus_rpc-1.2.0.tar.gz", hash = "sha256:b4ddaffa4d3996aaeadf49b80dfcdfbca48fe4cb616defaf3b3c5c2c8fc61890", size = 74142, upload-time = "2025-11-17T19:17:06.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/04/eaac430d0e6bf21265ae989427d37e94be5e41dc216879f1fbb6c5339942/nexus_rpc-1.2.0-py3-none-any.whl", hash = "sha256:977876f3af811ad1a09b2961d3d1ac9233bda43ff0febbb0c9906483b9d9f8a3", size = 28166, upload-time = "2025-11-17T19:17:05.64Z" }, +] + +[[package]] +name = "openai" +version = "2.24.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/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" }, +] + +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.60b1" +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/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-httpx" +version = "0.60b1" +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/86/08/11208bcfcab4fc2023252c3f322aa397fd9ad948355fea60f5fc98648603/opentelemetry_instrumentation_httpx-0.60b1.tar.gz", hash = "sha256:a506ebaf28c60112cbe70ad4f0338f8603f148938cb7b6794ce1051cd2b270ae", size = 20611, upload-time = "2025-12-11T13:37:01.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/59/b98e84eebf745ffc75397eaad4763795bff8a30cbf2373a50ed4e70646c5/opentelemetry_instrumentation_httpx-0.60b1-py3-none-any.whl", hash = "sha256:f37636dd742ad2af83d896ba69601ed28da51fa4e25d1ab62fde89ce413e275b", size = 15701, upload-time = "2025-12-11T13:36:04.56Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/fc/c47bb04a1d8a941a4061307e1eddfa331ed4d0ab13d8a9781e6db256940a/opentelemetry_util_http-0.60b1.tar.gz", hash = "sha256:0d97152ca8c8a41ced7172d29d3622a219317f74ae6bb3027cfbdcf22c3cc0d6", size = 11053, upload-time = "2025-12-11T13:37:25.115Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/5c/d3f1733665f7cd582ef0842fb1d2ed0bc1fba10875160593342d22bba375/opentelemetry_util_http-0.60b1-py3-none-any.whl", hash = "sha256:66381ba28550c91bee14dcba8979ace443444af1ed609226634596b4b0faf199", size = 8947, upload-time = "2025-12-11T13:36:37.151Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathable" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/55/b748445cb4ea6b125626f15379be7c96d1035d4fa3e8fee362fa92298abf/pathable-0.5.0.tar.gz", hash = "sha256:d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1", size = 16655, upload-time = "2026-02-20T08:47:00.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-py3-none-any.whl", hash = "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", size = 16867, upload-time = "2026-02-20T08:46:59.536Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, +] + +[[package]] +name = "py-key-value-aio" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" }, +] + +[package.optional-dependencies] +filetree = [ + { name = "aiofile" }, + { name = "anyio" }, +] +keyring = [ + { name = "keyring" }, +] +memory = [ + { name = "cachetools" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, +] + +[[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 = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[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.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-ai" +version = "1.64.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "ui", "vertexai", "xai"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/53/cf619c9eb24343fa8faff3c811c5c0dfc187cd9d8824e78b0e7b19095b98/pydantic_ai-1.64.0.tar.gz", hash = "sha256:6b507e7a5489df9ec6b9f64663930883904fbcdf4fc6f667defcaad058f23793", size = 12133, upload-time = "2026-03-02T23:59:49.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/eb/6c49d2a23594ebfdafaf9fd3f267e92c54ea970cf2d7891429b19c4ff90e/pydantic_ai-1.64.0-py3-none-any.whl", hash = "sha256:b7498e4c0440f45983f42a6ce49943909e93593d612349ad82f7f82892a893c6", size = 7227, upload-time = "2026-03-02T23:59:39.322Z" }, +] + +[[package]] +name = "pydantic-ai-slim" +version = "1.64.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "genai-prices" }, + { name = "griffelib" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pydantic-graph" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/56/629291aafc1271a3643c66fc22a25d737ce9f7a0fbb19eec14feefb4835b/pydantic_ai_slim-1.64.0.tar.gz", hash = "sha256:fc17a6bc307aeeec8d9feb12267bc2e0f1261f169310c185698017b4a507aea4", size = 430974, upload-time = "2026-03-02T23:59:51.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ca/d3a9676e6e94237d5e029fdd684d3aacce2b2ac7726a5fc36ef5bb558c6a/pydantic_ai_slim-1.64.0-py3-none-any.whl", hash = "sha256:8cc79027e63a903837dfcdf6a57e12bce544ec48c460414863ac3e7ae27d39d4", size = 561472, upload-time = "2026-03-02T23:59:43.176Z" }, +] + +[package.optional-dependencies] +ag-ui = [ + { name = "ag-ui-protocol" }, + { name = "starlette" }, +] +anthropic = [ + { name = "anthropic" }, +] +bedrock = [ + { name = "boto3" }, +] +cli = [ + { name = "argcomplete" }, + { name = "prompt-toolkit" }, + { name = "pyperclip" }, + { name = "rich" }, +] +cohere = [ + { name = "cohere", marker = "sys_platform != 'emscripten'" }, +] +evals = [ + { name = "pydantic-evals" }, +] +fastmcp = [ + { name = "fastmcp" }, +] +google = [ + { name = "google-genai" }, +] +groq = [ + { name = "groq" }, +] +huggingface = [ + { name = "huggingface-hub" }, +] +logfire = [ + { name = "logfire", extra = ["httpx"] }, +] +mcp = [ + { name = "mcp" }, +] +mistral = [ + { name = "mistralai" }, +] +openai = [ + { name = "openai" }, + { name = "tiktoken" }, +] +retries = [ + { name = "tenacity" }, +] +temporal = [ + { name = "temporalio" }, +] +ui = [ + { name = "starlette" }, +] +vertexai = [ + { name = "google-auth" }, + { name = "requests" }, +] +xai = [ + { name = "xai-sdk" }, +] + +[[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/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-evals" +version = "1.64.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "pydantic-ai-slim" }, + { name = "pyyaml" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/80/b76de50430c02f2cef42a7a39b0a7d40a6fed212d36780a53c4be7f57735/pydantic_evals-1.64.0.tar.gz", hash = "sha256:cb9aa135eeb27a1448919ea5cc41183d5976e09321b713c712c7fa2c9e9e7e07", size = 56370, upload-time = "2026-03-02T23:59:52.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d5/ec6f0f8dc1f5acc84aa3ff0540c51cc5ef38812c57c93df7bbe851db134a/pydantic_evals-1.64.0-py3-none-any.whl", hash = "sha256:2c056aaf523a56eb21db46e1e8a541a90b9e8ab8cbfc88166c5083e2ad64b3a1", size = 67278, upload-time = "2026-03-02T23:59:45.866Z" }, +] + +[[package]] +name = "pydantic-graph" +version = "1.64.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/45/32c7ac4e0aa777fad08617dbcef23b24517b50dde46dd561d913e20d4680/pydantic_graph-1.64.0.tar.gz", hash = "sha256:172013126a68583558a9a96fc8153c6ac26a10b94827b63ea8b9afbc03b0f7a5", size = 58529, upload-time = "2026-03-02T23:59:53.547Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/26/f7d2151a70d00d3df3b76340f3ad152d0ffe5bbc46d1ed56859930a9c712/pydantic_graph-1.64.0-py3-none-any.whl", hash = "sha256:1566906493122682419e4049d7b8a30216684d0f24a53b0c910320ee698bfaae", size = 72351, upload-time = "2026-03-02T23:59:47.645Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/63/06673d1eb6d8f83c0ea1f677d770e12565fb516928b4109c9e2055656a9e/pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5", size = 853363, upload-time = "2026-02-15T20:44:06.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f", size = 268877, upload-time = "2026-02-15T20:44:05.464Z" }, +] + +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[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/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { 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 = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2026.2.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/b8/845a927e078f5e5cc55d29f57becbfde0003d52806544531ab3f2da4503c/regex-2026.2.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fc48c500838be6882b32748f60a15229d2dea96e59ef341eaa96ec83538f498d", size = 488461, upload-time = "2026-02-28T02:15:48.405Z" }, + { url = "https://files.pythonhosted.org/packages/32/f9/8a0034716684e38a729210ded6222249f29978b24b684f448162ef21f204/regex-2026.2.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2afa673660928d0b63d84353c6c08a8a476ddfc4a47e11742949d182e6863ce8", size = 290774, upload-time = "2026-02-28T02:15:51.738Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ba/b27feefffbb199528dd32667cd172ed484d9c197618c575f01217fbe6103/regex-2026.2.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7ab218076eb0944549e7fe74cf0e2b83a82edb27e81cc87411f76240865e04d5", size = 288737, upload-time = "2026-02-28T02:15:53.534Z" }, + { url = "https://files.pythonhosted.org/packages/18/c5/65379448ca3cbfe774fcc33774dc8295b1ee97dc3237ae3d3c7b27423c9d/regex-2026.2.28-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d63db12e45a9b9f064bfe4800cefefc7e5f182052e4c1b774d46a40ab1d9bb", size = 782675, upload-time = "2026-02-28T02:15:55.488Z" }, + { url = "https://files.pythonhosted.org/packages/aa/30/6fa55bef48090f900fbd4649333791fc3e6467380b9e775e741beeb3231f/regex-2026.2.28-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:195237dc327858a7721bf8b0bbbef797554bc13563c3591e91cd0767bacbe359", size = 850514, upload-time = "2026-02-28T02:15:57.509Z" }, + { url = "https://files.pythonhosted.org/packages/a9/28/9ca180fb3787a54150209754ac06a42409913571fa94994f340b3bba4e1e/regex-2026.2.28-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b387a0d092dac157fb026d737dde35ff3e49ef27f285343e7c6401851239df27", size = 896612, upload-time = "2026-02-28T02:15:59.682Z" }, + { url = "https://files.pythonhosted.org/packages/46/b5/f30d7d3936d6deecc3ea7bea4f7d3c5ee5124e7c8de372226e436b330a55/regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3935174fa4d9f70525a4367aaff3cb8bc0548129d114260c29d9dfa4a5b41692", size = 791691, upload-time = "2026-02-28T02:16:01.752Z" }, + { url = "https://files.pythonhosted.org/packages/f5/34/96631bcf446a56ba0b2a7f684358a76855dfe315b7c2f89b35388494ede0/regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b2b23587b26496ff5fd40df4278becdf386813ec00dc3533fa43a4cf0e2ad3c", size = 783111, upload-time = "2026-02-28T02:16:03.651Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/f95cb7a85fe284d41cd2f3625e0f2ae30172b55dfd2af1d9b4eaef6259d7/regex-2026.2.28-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3b24bd7e9d85dc7c6a8bd2aa14ecd234274a0248335a02adeb25448aecdd420d", size = 767512, upload-time = "2026-02-28T02:16:05.616Z" }, + { url = "https://files.pythonhosted.org/packages/3d/af/a650f64a79c02a97f73f64d4e7fc4cc1984e64affab14075e7c1f9a2db34/regex-2026.2.28-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd477d5f79920338107f04aa645f094032d9e3030cc55be581df3d1ef61aa318", size = 773920, upload-time = "2026-02-28T02:16:08.325Z" }, + { url = "https://files.pythonhosted.org/packages/72/f8/3f9c2c2af37aedb3f5a1e7227f81bea065028785260d9cacc488e43e6997/regex-2026.2.28-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b49eb78048c6354f49e91e4b77da21257fecb92256b6d599ae44403cab30b05b", size = 846681, upload-time = "2026-02-28T02:16:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/8db04a334571359f4d127d8f89550917ec6561a2fddfd69cd91402b47482/regex-2026.2.28-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a25c7701e4f7a70021db9aaf4a4a0a67033c6318752146e03d1b94d32006217e", size = 755565, upload-time = "2026-02-28T02:16:11.972Z" }, + { url = "https://files.pythonhosted.org/packages/da/bc/91c22f384d79324121b134c267a86ca90d11f8016aafb1dc5bee05890ee3/regex-2026.2.28-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9dd450db6458387167e033cfa80887a34c99c81d26da1bf8b0b41bf8c9cac88e", size = 835789, upload-time = "2026-02-28T02:16:14.036Z" }, + { url = "https://files.pythonhosted.org/packages/46/a7/4cc94fd3af01dcfdf5a9ed75c8e15fd80fcd62cc46da7592b1749e9c35db/regex-2026.2.28-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2954379dd20752e82d22accf3ff465311cbb2bac6c1f92c4afd400e1757f7451", size = 780094, upload-time = "2026-02-28T02:16:15.468Z" }, + { url = "https://files.pythonhosted.org/packages/3c/21/e5a38f420af3c77cab4a65f0c3a55ec02ac9babf04479cfd282d356988a6/regex-2026.2.28-cp310-cp310-win32.whl", hash = "sha256:1f8b17be5c27a684ea6759983c13506bd77bfc7c0347dff41b18ce5ddd2ee09a", size = 266025, upload-time = "2026-02-28T02:16:16.828Z" }, + { url = "https://files.pythonhosted.org/packages/4d/0a/205c4c1466a36e04d90afcd01d8908bac327673050c7fe316b2416d99d3d/regex-2026.2.28-cp310-cp310-win_amd64.whl", hash = "sha256:dd8847c4978bc3c7e6c826fb745f5570e518b8459ac2892151ce6627c7bc00d5", size = 277965, upload-time = "2026-02-28T02:16:18.752Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4d/29b58172f954b6ec2c5ed28529a65e9026ab96b4b7016bcd3858f1c31d3c/regex-2026.2.28-cp310-cp310-win_arm64.whl", hash = "sha256:73cdcdbba8028167ea81490c7f45280113e41db2c7afb65a276f4711fa3bcbff", size = 270336, upload-time = "2026-02-28T02:16:20.735Z" }, + { url = "https://files.pythonhosted.org/packages/04/db/8cbfd0ba3f302f2d09dd0019a9fcab74b63fee77a76c937d0e33161fb8c1/regex-2026.2.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9", size = 488462, upload-time = "2026-02-28T02:16:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/5d/10/ccc22c52802223f2368731964ddd117799e1390ffc39dbb31634a83022ee/regex-2026.2.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97", size = 290774, upload-time = "2026-02-28T02:16:23.993Z" }, + { url = "https://files.pythonhosted.org/packages/62/b9/6796b3bf3101e64117201aaa3a5a030ec677ecf34b3cd6141b5d5c6c67d5/regex-2026.2.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703", size = 288724, upload-time = "2026-02-28T02:16:25.403Z" }, + { url = "https://files.pythonhosted.org/packages/9c/02/291c0ae3f3a10cea941d0f5366da1843d8d1fa8a25b0671e20a0e454bb38/regex-2026.2.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098", size = 791924, upload-time = "2026-02-28T02:16:26.863Z" }, + { url = "https://files.pythonhosted.org/packages/0f/57/f0235cc520d9672742196c5c15098f8f703f2758d48d5a7465a56333e496/regex-2026.2.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2", size = 860095, upload-time = "2026-02-28T02:16:28.772Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/393c94cbedda79a0f5f2435ebd01644aba0b338d327eb24b4aa5b8d6c07f/regex-2026.2.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64", size = 906583, upload-time = "2026-02-28T02:16:30.977Z" }, + { url = "https://files.pythonhosted.org/packages/2c/73/a72820f47ca5abf2b5d911d0407ba5178fc52cf9780191ed3a54f5f419a2/regex-2026.2.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022", size = 800234, upload-time = "2026-02-28T02:16:32.55Z" }, + { url = "https://files.pythonhosted.org/packages/34/b3/6e6a4b7b31fa998c4cf159a12cbeaf356386fbd1a8be743b1e80a3da51e4/regex-2026.2.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1", size = 772803, upload-time = "2026-02-28T02:16:34.029Z" }, + { url = "https://files.pythonhosted.org/packages/10/e7/5da0280c765d5a92af5e1cd324b3fe8464303189cbaa449de9a71910e273/regex-2026.2.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a", size = 781117, upload-time = "2026-02-28T02:16:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/76/39/0b8d7efb256ae34e1b8157acc1afd8758048a1cf0196e1aec2e71fd99f4b/regex-2026.2.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27", size = 854224, upload-time = "2026-02-28T02:16:38.119Z" }, + { url = "https://files.pythonhosted.org/packages/21/ff/a96d483ebe8fe6d1c67907729202313895d8de8495569ec319c6f29d0438/regex-2026.2.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae", size = 761898, upload-time = "2026-02-28T02:16:40.333Z" }, + { url = "https://files.pythonhosted.org/packages/89/bd/d4f2e75cb4a54b484e796017e37c0d09d8a0a837de43d17e238adf163f4e/regex-2026.2.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea", size = 844832, upload-time = "2026-02-28T02:16:41.875Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a7/428a135cf5e15e4e11d1e696eb2bf968362f8ea8a5f237122e96bc2ae950/regex-2026.2.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b", size = 788347, upload-time = "2026-02-28T02:16:43.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/59/68691428851cf9c9c3707217ab1d9b47cfeec9d153a49919e6c368b9e926/regex-2026.2.28-cp311-cp311-win32.whl", hash = "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15", size = 266033, upload-time = "2026-02-28T02:16:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/1483de1c57024e89296cbcceb9cccb3f625d416ddb46e570be185c9b05a9/regex-2026.2.28-cp311-cp311-win_amd64.whl", hash = "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61", size = 277978, upload-time = "2026-02-28T02:16:46.75Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/abec45dc6e7252e3dbc797120496e43bb5730a7abf0d9cb69340696a2f2d/regex-2026.2.28-cp311-cp311-win_arm64.whl", hash = "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a", size = 270340, upload-time = "2026-02-28T02:16:48.626Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/9061b03cf0fc4b5fa2c3984cbbaed54324377e440a5c5a29d29a72518d62/regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", size = 489574, upload-time = "2026-02-28T02:16:50.455Z" }, + { url = "https://files.pythonhosted.org/packages/77/83/0c8a5623a233015595e3da499c5a1c13720ac63c107897a6037bb97af248/regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", size = 291426, upload-time = "2026-02-28T02:16:52.52Z" }, + { url = "https://files.pythonhosted.org/packages/9e/06/3ef1ac6910dc3295ebd71b1f9bfa737e82cfead211a18b319d45f85ddd09/regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", size = 289200, upload-time = "2026-02-28T02:16:54.08Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c9/8cc8d850b35ab5650ff6756a1cb85286e2000b66c97520b29c1587455344/regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", size = 796765, upload-time = "2026-02-28T02:16:55.905Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5d/57702597627fc23278ebf36fbb497ac91c0ce7fec89ac6c81e420ca3e38c/regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", size = 863093, upload-time = "2026-02-28T02:16:58.094Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/f3ecad537ca2811b4d26b54ca848cf70e04fcfc138667c146a9f3157779c/regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", size = 909455, upload-time = "2026-02-28T02:17:00.918Z" }, + { url = "https://files.pythonhosted.org/packages/9e/40/bb226f203caa22c1043c1ca79b36340156eca0f6a6742b46c3bb222a3a57/regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", size = 802037, upload-time = "2026-02-28T02:17:02.842Z" }, + { url = "https://files.pythonhosted.org/packages/44/7c/c6d91d8911ac6803b45ca968e8e500c46934e58c0903cbc6d760ee817a0a/regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", size = 775113, upload-time = "2026-02-28T02:17:04.506Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/4a9368d168d47abd4158580b8c848709667b1cd293ff0c0c277279543bd0/regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", size = 784194, upload-time = "2026-02-28T02:17:06.888Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bf/2c72ab5d8b7be462cb1651b5cc333da1d0068740342f350fcca3bca31947/regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", size = 856846, upload-time = "2026-02-28T02:17:09.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f4/6b65c979bb6d09f51bb2d2a7bc85de73c01ec73335d7ddd202dcb8cd1c8f/regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", size = 763516, upload-time = "2026-02-28T02:17:11.004Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/29ea5e27400ee86d2cc2b4e80aa059df04eaf78b4f0c18576ae077aeff68/regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", size = 849278, upload-time = "2026-02-28T02:17:12.693Z" }, + { url = "https://files.pythonhosted.org/packages/1d/91/3233d03b5f865111cd517e1c95ee8b43e8b428d61fa73764a80c9bb6f537/regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", size = 790068, upload-time = "2026-02-28T02:17:14.9Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/abc706c1fb03b4580a09645b206a3fc032f5a9f457bc1a8038ac555658ab/regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", size = 266416, upload-time = "2026-02-28T02:17:17.15Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/2a6f7dff190e5fa9df9fb4acf2fdf17a1aa0f7f54596cba8de608db56b3a/regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", size = 277297, upload-time = "2026-02-28T02:17:18.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f0/58a2484851fadf284458fdbd728f580d55c1abac059ae9f048c63b92f427/regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", size = 270408, upload-time = "2026-02-28T02:17:20.328Z" }, + { url = "https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size = 489311, upload-time = "2026-02-28T02:17:22.591Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size = 291285, upload-time = "2026-02-28T02:17:24.355Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size = 289051, upload-time = "2026-02-28T02:17:26.722Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size = 796842, upload-time = "2026-02-28T02:17:29.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size = 863083, upload-time = "2026-02-28T02:17:31.363Z" }, + { url = "https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size = 909412, upload-time = "2026-02-28T02:17:33.248Z" }, + { url = "https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size = 802101, upload-time = "2026-02-28T02:17:35.053Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size = 775260, upload-time = "2026-02-28T02:17:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size = 784311, upload-time = "2026-02-28T02:17:39.855Z" }, + { url = "https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size = 856876, upload-time = "2026-02-28T02:17:42.317Z" }, + { url = "https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size = 763632, upload-time = "2026-02-28T02:17:45.073Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size = 849320, upload-time = "2026-02-28T02:17:47.192Z" }, + { url = "https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size = 790152, upload-time = "2026-02-28T02:17:49.067Z" }, + { url = "https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size = 266398, upload-time = "2026-02-28T02:17:50.744Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size = 277282, upload-time = "2026-02-28T02:17:53.074Z" }, + { url = "https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size = 270382, upload-time = "2026-02-28T02:17:54.888Z" }, + { url = "https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size = 492541, upload-time = "2026-02-28T02:17:56.813Z" }, + { url = "https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size = 292984, upload-time = "2026-02-28T02:17:58.538Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size = 291509, upload-time = "2026-02-28T02:18:00.208Z" }, + { url = "https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size = 809429, upload-time = "2026-02-28T02:18:02.328Z" }, + { url = "https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size = 869422, upload-time = "2026-02-28T02:18:04.23Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size = 915175, upload-time = "2026-02-28T02:18:06.791Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size = 812044, upload-time = "2026-02-28T02:18:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size = 782056, upload-time = "2026-02-28T02:18:10.777Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size = 798743, upload-time = "2026-02-28T02:18:13.025Z" }, + { url = "https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size = 864633, upload-time = "2026-02-28T02:18:16.84Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size = 770862, upload-time = "2026-02-28T02:18:18.892Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size = 854788, upload-time = "2026-02-28T02:18:21.475Z" }, + { url = "https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size = 800184, upload-time = "2026-02-28T02:18:23.492Z" }, + { url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137, upload-time = "2026-02-28T02:18:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682, upload-time = "2026-02-28T02:18:27.205Z" }, + { url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735, upload-time = "2026-02-28T02:18:29.015Z" }, + { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497, upload-time = "2026-02-28T02:18:30.889Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295, upload-time = "2026-02-28T02:18:33.426Z" }, + { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275, upload-time = "2026-02-28T02:18:35.247Z" }, + { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176, upload-time = "2026-02-28T02:18:37.15Z" }, + { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813, upload-time = "2026-02-28T02:18:39.478Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678, upload-time = "2026-02-28T02:18:41.619Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528, upload-time = "2026-02-28T02:18:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373, upload-time = "2026-02-28T02:18:46.102Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859, upload-time = "2026-02-28T02:18:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813, upload-time = "2026-02-28T02:18:50.576Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705, upload-time = "2026-02-28T02:18:52.59Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734, upload-time = "2026-02-28T02:18:54.595Z" }, + { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871, upload-time = "2026-02-28T02:18:57.34Z" }, + { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825, upload-time = "2026-02-28T02:18:59.202Z" }, + { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548, upload-time = "2026-02-28T02:19:01.049Z" }, + { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444, upload-time = "2026-02-28T02:19:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546, upload-time = "2026-02-28T02:19:05.378Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986, upload-time = "2026-02-28T02:19:07.24Z" }, + { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518, upload-time = "2026-02-28T02:19:09.698Z" }, + { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464, upload-time = "2026-02-28T02:19:12.494Z" }, + { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553, upload-time = "2026-02-28T02:19:15.151Z" }, + { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289, upload-time = "2026-02-28T02:19:17.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156, upload-time = "2026-02-28T02:19:20.011Z" }, + { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215, upload-time = "2026-02-28T02:19:22.047Z" }, + { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925, upload-time = "2026-02-28T02:19:24.173Z" }, + { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701, upload-time = "2026-02-28T02:19:26.376Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899, upload-time = "2026-02-28T02:19:29.38Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727, upload-time = "2026-02-28T02:19:31.494Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366, upload-time = "2026-02-28T02:19:34.248Z" }, + { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936, upload-time = "2026-02-28T02:19:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779, upload-time = "2026-02-28T02:19:38.625Z" }, + { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010, upload-time = "2026-02-28T02:19:40.65Z" }, +] + +[[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 = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "rich-rst" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, +] + +[[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/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[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 = "s3transfer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "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 = "sse-starlette" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/9f/c3695c2d2d4ef70072c3a06992850498b01c6bc9be531950813716b426fa/sse_starlette-3.3.2.tar.gz", hash = "sha256:678fca55a1945c734d8472a6cad186a55ab02840b4f6786f5ee8770970579dcd", size = 32326, upload-time = "2026-02-28T11:24:34.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/28/8cb142d3fe80c4a2d8af54ca0b003f47ce0ba920974e7990fa6e016402d1/sse_starlette-3.3.2-py3-none-any.whl", hash = "sha256:5c3ea3dad425c601236726af2f27689b74494643f57017cafcb6f8c9acfbb862", size = 14270, upload-time = "2026-02-28T11:24:32.984Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "temporalio" +version = "1.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nexus-rpc" }, + { name = "protobuf" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "types-protobuf" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/db/7d5118d28b0918888e1ec98f56f659fdb006351e06d95f30f4274962a76f/temporalio-1.20.0.tar.gz", hash = "sha256:5a6a85b7d298b7359bffa30025f7deac83c74ac095a4c6952fbf06c249a2a67c", size = 1850498, upload-time = "2025-11-25T21:25:20.225Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/1b/e69052aa6003eafe595529485d9c62d1382dd5e671108f1bddf544fb6032/temporalio-1.20.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:fba70314b4068f8b1994bddfa0e2ad742483f0ae714d2ef52e63013ccfd7042e", size = 12061638, upload-time = "2025-11-25T21:24:57.918Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/3e8c67ed7f23bedfa231c6ac29a7a9c12b89881da7694732270f3ecd6b0c/temporalio-1.20.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffc5bb6cabc6ae67f0bfba44de6a9c121603134ae18784a2ff3a7f230ad99080", size = 11562603, upload-time = "2025-11-25T21:25:01.721Z" }, + { url = "https://files.pythonhosted.org/packages/6d/be/ed0cc11702210522a79e09703267ebeca06eb45832b873a58de3ca76b9d0/temporalio-1.20.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1e80c1e4cdf88fa8277177f563edc91466fe4dc13c0322f26e55c76b6a219e6", size = 11824016, upload-time = "2025-11-25T21:25:06.771Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/09c5cafabc80139d97338a2bdd8ec22e08817dfd2949ab3e5b73565006eb/temporalio-1.20.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba92d909188930860c9d89ca6d7a753bc5a67e4e9eac6cea351477c967355eed", size = 12189521, upload-time = "2025-11-25T21:25:12.091Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/5689c014a76aff3b744b3ee0d80815f63b1362637814f5fbb105244df09b/temporalio-1.20.0-cp310-abi3-win_amd64.whl", hash = "sha256:eacfd571b653e0a0f4aa6593f4d06fc628797898f0900d400e833a1f40cad03a", size = 12745027, upload-time = "2025-11-25T21:25:16.827Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991, upload-time = "2025-10-06T20:21:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798, upload-time = "2025-10-06T20:21:35.579Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" }, + { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" }, + { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", size = 879375, upload-time = "2025-10-06T20:21:43.201Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "types-protobuf" +version = "6.32.1.20260221" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/e2/9aa4a3b2469508bd7b4e2ae11cbedaf419222a09a1b94daffcd5efca4023/types_protobuf-6.32.1.20260221.tar.gz", hash = "sha256:6d5fb060a616bfb076cbb61b4b3c3969f5fc8bec5810f9a2f7e648ee5cbcbf6e", size = 64408, upload-time = "2026-02-21T03:55:13.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/e8/1fd38926f9cf031188fbc5a96694203ea6f24b0e34bd64a225ec6f6291ba/types_protobuf-6.32.1.20260221-py3-none-any.whl", hash = "sha256:da7cdd947975964a93c30bfbcc2c6841ee646b318d3816b033adc2c4eb6448e4", size = 77956, upload-time = "2026-02-21T03:55:12.894Z" }, +] + +[[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 = "uncalled-for" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/7c/b5b7d8136f872e3f13b0584e576886de0489d7213a12de6bebf29ff6ebfc/uncalled_for-0.2.0.tar.gz", hash = "sha256:b4f8fdbcec328c5a113807d653e041c5094473dd4afa7c34599ace69ccb7e69f", size = 49488, upload-time = "2026-02-27T17:40:58.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/7f/4320d9ce3be404e6310b915c3629fe27bf1e2f438a1a7a3cb0396e32e9a9/uncalled_for-0.2.0-py3-none-any.whl", hash = "sha256:2c0bd338faff5f930918f79e7eb9ff48290df2cb05fcc0b40a7f334e55d4d85f", size = 11351, upload-time = "2026-02-27T17:40:56.804Z" }, +] + +[[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.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, +] + +[[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/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { 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/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { 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/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + +[[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/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { 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/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { 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 = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "xai-sdk" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-sdk" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/3e/cf9ffdf444ec3d495b8f4a6b3934c86088900aeb131cf2d81535b218e615/xai_sdk-1.7.0.tar.gz", hash = "sha256:649e7c3bff01510326f13a709dff824256fcdfd2850e21606d25870208989bd1", size = 390582, upload-time = "2026-02-18T20:53:23.352Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/e9/e0fb57dc5b2580aa6390a836cf81d507f8017888ab8197600489118cc677/xai_sdk-1.7.0-py3-none-any.whl", hash = "sha256:332672c46ee73a7aad9b05e82db5a5044174bb3f5f038a7acf703f0b0ce6bad0", size = 241650, upload-time = "2026-02-18T20:53:22.016Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0d/9cc638702f6fc3c7a3685bcc8cf2a9ed7d6206e932a49f5242658047ef51/yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", size = 123764, upload-time = "2026-03-01T22:04:09.7Z" }, + { url = "https://files.pythonhosted.org/packages/7a/35/5a553687c5793df5429cd1db45909d4f3af7eee90014888c208d086a44f0/yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", size = 86282, upload-time = "2026-03-01T22:04:11.892Z" }, + { url = "https://files.pythonhosted.org/packages/68/2e/c5a2234238f8ce37a8312b52801ee74117f576b1539eec8404a480434acc/yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", size = 86053, upload-time = "2026-03-01T22:04:13.292Z" }, + { url = "https://files.pythonhosted.org/packages/74/3f/bbd8ff36fb038622797ffbaf7db314918bb4d76f1cc8a4f9ca7a55fe5195/yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", size = 99395, upload-time = "2026-03-01T22:04:15.133Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/9516bc4e269d2a3ec9c6779fcdeac51ce5b3a9b0156f06ac7152e5bba864/yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", size = 92143, upload-time = "2026-03-01T22:04:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/c7/63/88802d1f6b1cb1fc67d67a58cd0cf8a1790de4ce7946e434240f1d60ab4a/yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", size = 107643, upload-time = "2026-03-01T22:04:18.519Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/4f9b838f4d8bdd6f0f385aed8bbf21c71ed11a0b9983305c302cbd557815/yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", size = 108700, upload-time = "2026-03-01T22:04:20.373Z" }, + { url = "https://files.pythonhosted.org/packages/50/12/95a1d33f04a79c402664070d43b8b9f72dc18914e135b345b611b0b1f8cc/yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", size = 102769, upload-time = "2026-03-01T22:04:23.055Z" }, + { url = "https://files.pythonhosted.org/packages/86/65/91a0285f51321369fd1a8308aa19207520c5f0587772cfc2e03fc2467e90/yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", size = 101114, upload-time = "2026-03-01T22:04:25.031Z" }, + { url = "https://files.pythonhosted.org/packages/58/80/c7c8244fc3e5bc483dc71a09560f43b619fab29301a0f0a8f936e42865c7/yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", size = 98883, upload-time = "2026-03-01T22:04:27.281Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/71ca9cc9ca79c0b7d491216177d1aed559d632947b8ffb0ee60f7d8b23e3/yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", size = 94172, upload-time = "2026-03-01T22:04:28.554Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3f/6c6c8a0fe29c26fb2db2e8d32195bb84ec1bfb8f1d32e7f73b787fcf349b/yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", size = 107010, upload-time = "2026-03-01T22:04:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/56/38/12730c05e5ad40a76374d440ed8b0899729a96c250516d91c620a6e38fc2/yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", size = 100285, upload-time = "2026-03-01T22:04:31.752Z" }, + { url = "https://files.pythonhosted.org/packages/34/92/6a7be9239f2347234e027284e7a5f74b1140cc86575e7b469d13fba1ebfe/yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", size = 108230, upload-time = "2026-03-01T22:04:33.844Z" }, + { url = "https://files.pythonhosted.org/packages/5e/81/4aebccfa9376bd98b9d8bfad20621a57d3e8cfc5b8631c1fa5f62cdd03f4/yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", size = 103008, upload-time = "2026-03-01T22:04:35.856Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/0b4e3edcec794a86b853b0c6396c0a888d72dfce19b2d88c02ac289fb6c1/yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", size = 83073, upload-time = "2026-03-01T22:04:38.268Z" }, + { url = "https://files.pythonhosted.org/packages/a0/71/ad95c33da18897e4c636528bbc24a1dd23fe16797de8bc4ec667b8db0ba4/yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", size = 87328, upload-time = "2026-03-01T22:04:39.558Z" }, + { url = "https://files.pythonhosted.org/packages/e2/14/dfa369523c79bccf9c9c746b0a63eb31f65db9418ac01275f7950962e504/yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", size = 82463, upload-time = "2026-03-01T22:04:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, + { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, + { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, + { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +] + +[[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" }, +]